autobyteus 1.1.4__py3-none-any.whl → 1.1.5__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 (167) hide show
  1. autobyteus/agent/context/__init__.py +4 -2
  2. autobyteus/agent/context/agent_config.py +0 -4
  3. autobyteus/agent/context/agent_context_registry.py +73 -0
  4. autobyteus/agent/events/notifiers.py +4 -0
  5. autobyteus/agent/handlers/inter_agent_message_event_handler.py +7 -2
  6. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
  7. autobyteus/agent/handlers/user_input_message_event_handler.py +15 -0
  8. autobyteus/agent/message/send_message_to.py +29 -23
  9. autobyteus/agent/runtime/agent_runtime.py +10 -2
  10. autobyteus/agent/sender_type.py +15 -0
  11. autobyteus/agent/streaming/agent_event_stream.py +6 -0
  12. autobyteus/agent/streaming/stream_event_payloads.py +12 -0
  13. autobyteus/agent/streaming/stream_events.py +3 -0
  14. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
  15. autobyteus/agent_team/__init__.py +1 -0
  16. autobyteus/agent_team/agent_team.py +93 -0
  17. autobyteus/agent_team/agent_team_builder.py +184 -0
  18. autobyteus/agent_team/base_agent_team.py +86 -0
  19. autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
  20. autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
  21. autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
  22. autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
  23. autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
  24. autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
  25. autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
  26. autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
  27. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
  28. autobyteus/agent_team/context/__init__.py +17 -0
  29. autobyteus/agent_team/context/agent_team_config.py +33 -0
  30. autobyteus/agent_team/context/agent_team_context.py +61 -0
  31. autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
  32. autobyteus/agent_team/context/team_manager.py +147 -0
  33. autobyteus/agent_team/context/team_node_config.py +76 -0
  34. autobyteus/agent_team/events/__init__.py +29 -0
  35. autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
  36. autobyteus/agent_team/events/agent_team_events.py +53 -0
  37. autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
  38. autobyteus/agent_team/exceptions.py +8 -0
  39. autobyteus/agent_team/factory/__init__.py +9 -0
  40. autobyteus/agent_team/factory/agent_team_factory.py +99 -0
  41. autobyteus/agent_team/handlers/__init__.py +19 -0
  42. autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
  43. autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
  44. autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
  45. autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
  46. autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
  47. autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
  48. autobyteus/agent_team/phases/__init__.py +11 -0
  49. autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
  50. autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
  51. autobyteus/agent_team/runtime/__init__.py +13 -0
  52. autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
  53. autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
  54. autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
  55. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
  56. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
  57. autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
  58. autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
  59. autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
  60. autobyteus/agent_team/streaming/__init__.py +26 -0
  61. autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
  62. autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
  63. autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
  64. autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
  65. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
  66. autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
  67. autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
  68. autobyteus/agent_team/task_notification/__init__.py +11 -0
  69. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
  70. autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
  71. autobyteus/agent_team/utils/__init__.py +9 -0
  72. autobyteus/agent_team/utils/wait_for_idle.py +46 -0
  73. autobyteus/cli/agent_team_tui/__init__.py +4 -0
  74. autobyteus/cli/agent_team_tui/app.py +210 -0
  75. autobyteus/cli/agent_team_tui/state.py +180 -0
  76. autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
  77. autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
  78. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
  79. autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
  80. autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
  81. autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
  82. autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
  83. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
  84. autobyteus/events/event_types.py +7 -2
  85. autobyteus/llm/api/autobyteus_llm.py +11 -12
  86. autobyteus/llm/api/lmstudio_llm.py +10 -13
  87. autobyteus/llm/api/ollama_llm.py +8 -13
  88. autobyteus/llm/autobyteus_provider.py +73 -46
  89. autobyteus/llm/llm_factory.py +102 -140
  90. autobyteus/llm/lmstudio_provider.py +63 -48
  91. autobyteus/llm/models.py +83 -53
  92. autobyteus/llm/ollama_provider.py +69 -61
  93. autobyteus/llm/ollama_provider_resolver.py +1 -0
  94. autobyteus/llm/providers.py +13 -13
  95. autobyteus/llm/runtimes.py +11 -0
  96. autobyteus/task_management/__init__.py +43 -0
  97. autobyteus/task_management/base_task_board.py +68 -0
  98. autobyteus/task_management/converters/__init__.py +11 -0
  99. autobyteus/task_management/converters/task_board_converter.py +64 -0
  100. autobyteus/task_management/converters/task_plan_converter.py +48 -0
  101. autobyteus/task_management/deliverable.py +16 -0
  102. autobyteus/task_management/deliverables/__init__.py +8 -0
  103. autobyteus/task_management/deliverables/file_deliverable.py +15 -0
  104. autobyteus/task_management/events.py +27 -0
  105. autobyteus/task_management/in_memory_task_board.py +126 -0
  106. autobyteus/task_management/schemas/__init__.py +15 -0
  107. autobyteus/task_management/schemas/deliverable_schema.py +13 -0
  108. autobyteus/task_management/schemas/plan_definition.py +35 -0
  109. autobyteus/task_management/schemas/task_status_report.py +27 -0
  110. autobyteus/task_management/task_plan.py +110 -0
  111. autobyteus/task_management/tools/__init__.py +14 -0
  112. autobyteus/task_management/tools/get_task_board_status.py +68 -0
  113. autobyteus/task_management/tools/publish_task_plan.py +113 -0
  114. autobyteus/task_management/tools/update_task_status.py +135 -0
  115. autobyteus/tools/bash/bash_executor.py +59 -14
  116. autobyteus/tools/mcp/config_service.py +63 -58
  117. autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
  118. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
  119. autobyteus/tools/mcp/server_instance_manager.py +30 -4
  120. autobyteus/tools/mcp/tool_registrar.py +103 -50
  121. autobyteus/tools/parameter_schema.py +17 -11
  122. autobyteus/tools/registry/tool_definition.py +24 -29
  123. autobyteus/tools/tool_category.py +1 -0
  124. autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
  125. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
  126. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
  127. autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
  128. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
  129. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
  130. autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
  131. autobyteus/tools/usage/providers/__init__.py +2 -12
  132. autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
  133. autobyteus/tools/usage/registries/__init__.py +7 -12
  134. autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
  135. autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
  136. autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
  137. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/METADATA +3 -3
  138. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/RECORD +146 -72
  139. examples/agent_team/__init__.py +1 -0
  140. examples/run_browser_agent.py +17 -15
  141. examples/run_google_slides_agent.py +17 -16
  142. examples/run_poem_writer.py +22 -12
  143. examples/run_sqlite_agent.py +17 -15
  144. autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
  145. autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
  146. autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
  147. autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
  148. autobyteus/tools/usage/providers/json_example_provider.py +0 -32
  149. autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
  150. autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
  151. autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
  152. autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
  153. autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
  154. autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
  155. autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
  156. autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
  157. autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
  158. autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
  159. autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
  160. examples/workflow/__init__.py +0 -1
  161. examples/workflow/run_basic_research_workflow.py +0 -189
  162. examples/workflow/run_code_review_workflow.py +0 -269
  163. examples/workflow/run_debate_workflow.py +0 -212
  164. examples/workflow/run_workflow_with_tui.py +0 -153
  165. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
  166. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
  167. {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,164 @@
1
+ # file: autobyteus/autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py
2
+ import asyncio
3
+ import logging
4
+ from typing import Set, Any, TYPE_CHECKING, List, Union
5
+
6
+ from autobyteus.events.event_types import EventType
7
+ from autobyteus.agent_team.events import ProcessUserMessageEvent
8
+ from autobyteus.agent.message import AgentInputUserMessage
9
+ from autobyteus.task_management.events import TaskPlanPublishedEvent, TaskStatusUpdatedEvent
10
+ from autobyteus.task_management.base_task_board import TaskStatus
11
+ from autobyteus.task_management.task_plan import Task
12
+
13
+ if TYPE_CHECKING:
14
+ from autobyteus.task_management.base_task_board import BaseTaskBoard
15
+ from autobyteus.agent_team.context.team_manager import TeamManager
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class SystemEventDrivenAgentTaskNotifier:
20
+ """
21
+ An internal component that monitors a TaskBoard and automatically sends
22
+ notifications to agents when their assigned tasks become runnable.
23
+ """
24
+ def __init__(self, task_board: 'BaseTaskBoard', team_manager: 'TeamManager'):
25
+ """
26
+ Initializes the SystemEventDrivenAgentTaskNotifier.
27
+
28
+ Args:
29
+ task_board: The team's shared task board instance.
30
+ team_manager: The team's manager for submitting notification events.
31
+ """
32
+ if not task_board or not team_manager:
33
+ raise ValueError("TaskBoard and TeamManager are required for the notifier.")
34
+
35
+ self._task_board = task_board
36
+ self._team_manager = team_manager
37
+ self._dispatched_task_ids: Set[str] = set()
38
+ logger.info(f"SystemEventDrivenAgentTaskNotifier initialized for team '{self._team_manager.team_id}'.")
39
+
40
+ def start_monitoring(self):
41
+ """
42
+ Subscribes to task board events to begin monitoring for runnable tasks.
43
+ This should be called once during the agent team's bootstrap process.
44
+ """
45
+ self._task_board.subscribe(
46
+ EventType.TASK_BOARD_PLAN_PUBLISHED,
47
+ self._handle_task_board_update
48
+ )
49
+ self._task_board.subscribe(
50
+ EventType.TASK_BOARD_STATUS_UPDATED,
51
+ self._handle_task_board_update
52
+ )
53
+ logger.info(f"Team '{self._team_manager.team_id}': Task notifier is now monitoring TaskBoard events.")
54
+
55
+ async def _handle_task_board_update(self, payload: Union[TaskPlanPublishedEvent, TaskStatusUpdatedEvent], **kwargs):
56
+ """
57
+ Asynchronous event handler triggered by the task board. It uses the event
58
+ payload to decide when to check for and notify agents of newly runnable tasks.
59
+ """
60
+ if isinstance(payload, TaskPlanPublishedEvent):
61
+ logger.info(f"Team '{self._team_manager.team_id}': New task plan detected. Resetting dispatched tasks and checking for initial runnable tasks.")
62
+ self._dispatched_task_ids.clear()
63
+ await self._scan_and_notify_all_runnable_tasks()
64
+
65
+ elif isinstance(payload, TaskStatusUpdatedEvent):
66
+ # Only trigger a check for dependent tasks if a task has been completed,
67
+ # as this is the only status change that can unblock dependent tasks.
68
+ if payload.new_status == TaskStatus.COMPLETED:
69
+ logger.info(f"Team '{self._team_manager.team_id}': Task '{payload.task_id}' completed. Checking for newly unblocked dependent tasks.")
70
+ await self._check_and_notify_dependent_tasks(payload.task_id)
71
+ else:
72
+ logger.debug(f"Team '{self._team_manager.team_id}': Task '{payload.task_id}' status updated to '{payload.new_status.value}'. No dependent task check needed.")
73
+ else:
74
+ # This case should ideally not be hit with the new strong typing, but is kept as a safeguard.
75
+ logger.warning(f"Team '{self._team_manager.team_id}': Task notifier received an unhandled payload type: {type(payload)}")
76
+
77
+
78
+ async def _check_and_notify_dependent_tasks(self, completed_task_id: str):
79
+ """
80
+ Finds tasks that depend on the completed task and notifies their assignees
81
+ if all of their other dependencies are also met.
82
+ """
83
+ if not getattr(self._task_board, 'current_plan', None):
84
+ return
85
+
86
+ all_tasks = self._task_board.current_plan.tasks
87
+ task_statuses = getattr(self._task_board, 'task_statuses', {})
88
+
89
+ for child_task in all_tasks:
90
+ # Find tasks that are direct children of the completed task
91
+ if completed_task_id in child_task.dependencies:
92
+ # Now, check if this child task is fully runnable (all its parents are done)
93
+ all_deps_met = all(
94
+ task_statuses.get(dep_id) == TaskStatus.COMPLETED for dep_id in child_task.dependencies
95
+ )
96
+
97
+ if all_deps_met and child_task.task_id not in self._dispatched_task_ids:
98
+ await self._dispatch_notification_for_task(child_task)
99
+
100
+ async def _scan_and_notify_all_runnable_tasks(self):
101
+ """
102
+ Scans the entire board for any runnable tasks. Used for initial plan loading.
103
+ """
104
+ try:
105
+ runnable_tasks = self._task_board.get_next_runnable_tasks()
106
+ for task in runnable_tasks:
107
+ if task.task_id not in self._dispatched_task_ids:
108
+ await self._dispatch_notification_for_task(task)
109
+ except Exception as e:
110
+ logger.error(f"Team '{self._team_manager.team_id}': Error during full scan for runnable tasks: {e}", exc_info=True)
111
+
112
+ async def _dispatch_notification_for_task(self, task: Task):
113
+ """
114
+ Constructs and sends a context-rich notification for a single runnable task
115
+ by treating it as a user message to trigger the full processing pipeline.
116
+ It tags the message with metadata to indicate its system origin.
117
+ """
118
+ try:
119
+ team_id = self._team_manager.team_id
120
+ logger.info(f"Team '{team_id}': Dispatching notification for runnable task '{task.task_name}' to assignee '{task.assignee_name}'.")
121
+
122
+ context_from_parents = []
123
+ if task.dependencies:
124
+ parent_task_deliverables_info = []
125
+ for dep_id in task.dependencies:
126
+ parent_task = getattr(self._task_board, '_task_map', {}).get(dep_id)
127
+ if parent_task and parent_task.file_deliverables:
128
+ deliverables_str = "\n".join(
129
+ [f" - File: {d.file_path}, Summary: {d.summary}" for d in parent_task.file_deliverables]
130
+ )
131
+ parent_task_deliverables_info.append(
132
+ f"The parent task '{parent_task.task_name}' produced the following deliverables:\n{deliverables_str}"
133
+ )
134
+
135
+ if parent_task_deliverables_info:
136
+ context_from_parents.append(
137
+ "Your task is now unblocked. Here is the context from the completed parent task(s):\n" +
138
+ "\n\n".join(parent_task_deliverables_info)
139
+ )
140
+
141
+ message_parts: List[str] = [f"Your task '{task.task_name}' is now ready to start."]
142
+ if context_from_parents:
143
+ message_parts.extend(context_from_parents)
144
+
145
+ message_parts.append(f"\nYour task description:\n{task.description}")
146
+
147
+ content = "\n\n".join(message_parts)
148
+
149
+ # Create the user message with metadata indicating its origin.
150
+ user_message = AgentInputUserMessage(
151
+ content=content,
152
+ metadata={'source': 'system_task_notifier'}
153
+ )
154
+ event = ProcessUserMessageEvent(
155
+ user_message=user_message,
156
+ target_agent_name=task.assignee_name
157
+ )
158
+
159
+ # Use the existing method for dispatching user messages.
160
+ await self._team_manager.dispatch_user_message_to_agent(event)
161
+ self._dispatched_task_ids.add(task.task_id)
162
+
163
+ except Exception as e:
164
+ logger.error(f"Team '{self._team_manager.team_id}': Failed to dispatch notification for task '{task.task_id}': {e}", exc_info=True)
@@ -0,0 +1,24 @@
1
+ # file: autobyteus/autobyteus/agent_team/task_notification/task_notification_mode.py
2
+ """
3
+ Defines the enum for controlling how task notifications are handled in an agent team.
4
+ """
5
+ from enum import Enum
6
+
7
+ class TaskNotificationMode(str, Enum):
8
+ """
9
+ Enumerates the modes for handling task notifications within an agent team.
10
+ """
11
+ AGENT_MANUAL_NOTIFICATION = "agent_manual_notification"
12
+ """
13
+ In this mode, an agent (typically the coordinator) is responsible for
14
+ manually sending notifications to other agents to start their tasks.
15
+ """
16
+
17
+ SYSTEM_EVENT_DRIVEN = "system_event_driven"
18
+ """
19
+ In this mode, the agent team framework automatically monitors the TaskBoard
20
+ and sends notifications to agents when their assigned tasks become runnable.
21
+ """
22
+
23
+ def __str__(self) -> str:
24
+ return self.value
@@ -0,0 +1,9 @@
1
+ # file: autobyteus/autobyteus/agent_team/utils/__init__.py
2
+ """
3
+ Utility functions for interacting with agent teams.
4
+ """
5
+ from .wait_for_idle import wait_for_team_to_be_idle
6
+
7
+ __all__ = [
8
+ "wait_for_team_to_be_idle",
9
+ ]
@@ -0,0 +1,46 @@
1
+ # file: autobyteus/autobyteus/agent_team/utils/wait_for_idle.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.agent_team.streaming.agent_team_event_stream import AgentTeamEventStream
7
+ from autobyteus.agent_team.phases.agent_team_operational_phase import AgentTeamOperationalPhase
8
+
9
+ if TYPE_CHECKING:
10
+ from autobyteus.agent_team.agent_team import AgentTeam
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ async def _wait_loop(streamer: AgentTeamEventStream, team_id: str):
15
+ """Internal helper to listen for the IDLE or ERROR event."""
16
+ async for event in streamer.all_events():
17
+ if event.event_source_type == "TEAM" and event.data.new_phase == AgentTeamOperationalPhase.IDLE:
18
+ logger.info(f"Team '{team_id}' has become idle.")
19
+ return
20
+ if event.event_source_type == "TEAM" and event.data.new_phase == AgentTeamOperationalPhase.ERROR:
21
+ error_message = f"Team '{team_id}' entered an error state while waiting for idle: {event.data.error_message}"
22
+ logger.error(error_message)
23
+ raise RuntimeError(error_message)
24
+
25
+ async def wait_for_team_to_be_idle(team: 'AgentTeam', timeout: float = 60.0):
26
+ """
27
+ Waits for an agent team to complete its bootstrapping and enter the IDLE state.
28
+
29
+ Args:
30
+ team: The agent team instance to monitor.
31
+ timeout: The maximum time in seconds to wait.
32
+
33
+ Raises:
34
+ asyncio.TimeoutError: If the team does not become idle within the timeout period.
35
+ RuntimeError: If the team enters an error state.
36
+ """
37
+ if team.get_current_phase() == AgentTeamOperationalPhase.IDLE:
38
+ return
39
+
40
+ logger.info(f"Waiting for team '{team.team_id}' to become idle (timeout: {timeout}s)...")
41
+
42
+ streamer = AgentTeamEventStream(team)
43
+ try:
44
+ await asyncio.wait_for(_wait_loop(streamer, team.team_id), timeout=timeout)
45
+ finally:
46
+ await streamer.close()
@@ -0,0 +1,4 @@
1
+ # file: autobyteus/autobyteus/cli/agent_team_tui/__init__.py
2
+ """
3
+ A Textual-based TUI for interacting with Agent Teams.
4
+ """
@@ -0,0 +1,210 @@
1
+ # file: autobyteus/autobyteus/cli/agent_team_tui/app.py
2
+ """
3
+ The main Textual application class for the agent team TUI. This class orchestrates
4
+ the UI by reacting to changes in a central state store.
5
+ """
6
+ import asyncio
7
+ import logging
8
+ from typing import Dict, Optional, Any
9
+
10
+ from textual.app import App, ComposeResult
11
+ from textual.containers import Horizontal
12
+ from textual.widgets import Header, Static
13
+ from textual.reactive import reactive
14
+
15
+ from autobyteus.agent_team.agent_team import AgentTeam
16
+ from autobyteus.agent_team.streaming.agent_team_event_stream import AgentTeamEventStream
17
+ from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
18
+ from autobyteus.agent.streaming.stream_events import StreamEventType as AgentStreamEventType
19
+ from autobyteus.agent.streaming.stream_event_payloads import AssistantChunkData
20
+ from autobyteus.agent_team.streaming.agent_team_stream_event_payloads import AgentEventRebroadcastPayload, AgentTeamPhaseTransitionData
21
+
22
+ from .state import TUIStateStore
23
+ from .widgets.agent_list_sidebar import AgentListSidebar
24
+ from .widgets.focus_pane import FocusPane
25
+ from .widgets.status_bar import StatusBar
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ class AgentTeamApp(App):
30
+ """A Textual TUI for interacting with an agent team, built around a central state store."""
31
+
32
+ TITLE = "AutoByteus"
33
+ CSS_PATH = "app.css"
34
+ BINDINGS = [
35
+ ("d", "toggle_dark", "Toggle Dark Mode"),
36
+ ("q", "quit", "Quit"),
37
+ ]
38
+
39
+ focused_node_data: reactive[Optional[Dict[str, Any]]] = reactive(None)
40
+ # The store_version property will trigger UI updates for the sidebar.
41
+ store_version: reactive[int] = reactive(0)
42
+
43
+ def __init__(self, team: AgentTeam, **kwargs):
44
+ super().__init__(**kwargs)
45
+ self.team = team
46
+ self.store = TUIStateStore(team=self.team)
47
+ self.team_stream: Optional[AgentTeamEventStream] = None
48
+ # Flag to indicate that the UI needs an update, used for throttling.
49
+ self._ui_update_pending = False
50
+
51
+ def compose(self) -> ComposeResult:
52
+ yield Header(id="app-header", name="AutoByteus Mission Control")
53
+ with Horizontal(id="main-container"):
54
+ yield AgentListSidebar(id="sidebar")
55
+ yield FocusPane(id="focus-pane")
56
+ yield StatusBar()
57
+
58
+ async def on_mount(self) -> None:
59
+ """Start background tasks when the app is mounted."""
60
+ self.team.start()
61
+ self.team_stream = AgentTeamEventStream(self.team)
62
+
63
+ # Initialize the UI with the starting state
64
+ initial_tree = self.store.get_tree_data()
65
+ initial_focus_node = initial_tree.get(self.team.name)
66
+
67
+ self.store.set_focused_node(initial_focus_node)
68
+ self.focused_node_data = initial_focus_node
69
+ self.store_version = self.store.version # Trigger initial render
70
+
71
+ self.run_worker(self._listen_for_team_events(), name="team_listener")
72
+
73
+ # Set up a timer to run the throttled UI updater at ~15 FPS.
74
+ self.set_interval(1 / 15, self._throttled_ui_updater, name="ui_updater")
75
+ logger.info("Agent Team TUI mounted, team listener and throttled UI updater started.")
76
+
77
+ async def on_unmount(self) -> None:
78
+ if self.team and self.team.is_running:
79
+ await self.team.stop()
80
+
81
+ def _throttled_ui_updater(self) -> None:
82
+ """
83
+ Periodically checks if the UI state is dirty and, if so, triggers
84
+ reactive updates. It also flushes streaming buffers from the focus pane.
85
+ """
86
+ focus_pane = self.query_one(FocusPane)
87
+ if self._ui_update_pending:
88
+ self._ui_update_pending = False
89
+ # This is the throttled trigger for the async watcher.
90
+ self.store_version = self.store.version
91
+
92
+ # Always flush the focus pane's streaming buffer for smooth text rendering.
93
+ focus_pane.flush_stream_buffers()
94
+
95
+ async def _listen_for_team_events(self) -> None:
96
+ """A background worker that forwards team events to the state store and updates the UI."""
97
+ if not self.team_stream: return
98
+ try:
99
+ async for event in self.team_stream.all_events():
100
+ # 1. Always update the central state store immediately.
101
+ self.store.process_event(event)
102
+
103
+ # 2. Mark the UI as needing an update for the throttled components.
104
+ self._ui_update_pending = True
105
+
106
+ # 3. Handle real-time, incremental updates directly.
107
+ if isinstance(event.data, AgentEventRebroadcastPayload):
108
+ payload = event.data
109
+ agent_name = payload.agent_name
110
+ agent_event = payload.agent_event
111
+ focus_pane = self.query_one(FocusPane)
112
+
113
+ is_currently_focused = (focus_pane._focused_node_data and focus_pane._focused_node_data.get('name') == agent_name)
114
+
115
+ if is_currently_focused:
116
+ await focus_pane.add_agent_event(agent_event)
117
+
118
+ except asyncio.CancelledError:
119
+ logger.info("Agent team event listener task was cancelled.")
120
+ except Exception:
121
+ logger.error("Critical error in agent team TUI event listener", exc_info=True)
122
+ finally:
123
+ if self.team_stream: await self.team_stream.close()
124
+
125
+ # --- Reactive Watchers ---
126
+
127
+ async def watch_store_version(self, new_version: int):
128
+ """
129
+ Reacts to changes in the store version.
130
+ """
131
+ sidebar = self.query_one(AgentListSidebar)
132
+ focus_pane = self.query_one(FocusPane)
133
+
134
+ tree_data = self.store.get_tree_data()
135
+ agent_phases = self.store._agent_phases
136
+ team_phases = self.store._team_phases
137
+ speaking_agents = self.store._speaking_agents
138
+
139
+ sidebar.update_tree(tree_data, agent_phases, team_phases, speaking_agents)
140
+
141
+ focused_data = self.focused_node_data
142
+ if focused_data and focused_data.get("type") in ['team', 'subteam']:
143
+ node_name = focused_data['name']
144
+ task_plan = self.store.get_task_board_plan(node_name)
145
+ task_statuses = self.store.get_task_board_statuses(node_name)
146
+ await focus_pane.update_content(
147
+ node_data=focused_data,
148
+ history=[], # No history for teams
149
+ pending_approval=None,
150
+ all_agent_phases=agent_phases,
151
+ all_team_phases=team_phases,
152
+ task_plan=task_plan,
153
+ task_statuses=task_statuses
154
+ )
155
+ elif focused_data and focused_data.get("type") == 'agent':
156
+ focus_pane.update_current_node_status(agent_phases, team_phases)
157
+
158
+
159
+ async def watch_focused_node_data(self, new_node_data: Optional[Dict[str, Any]]):
160
+ """Reacts to changes in which node is focused. Primarily used for full pane reloads on user click."""
161
+ if not new_node_data: return
162
+
163
+ node_name = new_node_data['name']
164
+ node_type = new_node_data['type']
165
+
166
+ history = self.store.get_history_for_node(node_name, node_type)
167
+ pending_approval = self.store.get_pending_approval_for_agent(node_name) if node_type == 'agent' else None
168
+
169
+ task_plan = None
170
+ task_statuses = None
171
+ if node_type in ['team', 'subteam']:
172
+ task_plan = self.store.get_task_board_plan(node_name)
173
+ task_statuses = self.store.get_task_board_statuses(node_name)
174
+
175
+ sidebar = self.query_one(AgentListSidebar)
176
+ focus_pane = self.query_one(FocusPane)
177
+
178
+ await focus_pane.update_content(
179
+ node_data=new_node_data,
180
+ history=history,
181
+ pending_approval=pending_approval,
182
+ all_agent_phases=self.store._agent_phases,
183
+ all_team_phases=self.store._team_phases,
184
+ task_plan=task_plan,
185
+ task_statuses=task_statuses
186
+ )
187
+
188
+ sidebar.update_selection(node_name)
189
+
190
+ # --- Event Handlers (Actions) ---
191
+
192
+ def on_agent_list_sidebar_node_selected(self, message: AgentListSidebar.NodeSelected):
193
+ """Handles a node being selected by updating the store and the app's reactive state."""
194
+ self.store.set_focused_node(message.node_data)
195
+ self.focused_node_data = message.node_data
196
+
197
+ async def on_focus_pane_message_submitted(self, message: FocusPane.MessageSubmitted):
198
+ """Dispatches a user message to the backend model."""
199
+ user_message = AgentInputUserMessage(content=message.text)
200
+ await self.team.post_message(message=user_message, target_agent_name=message.agent_name)
201
+
202
+ async def on_focus_pane_approval_submitted(self, message: FocusPane.ApprovalSubmitted):
203
+ """Dispatches a tool approval to the backend model."""
204
+ self.store.clear_pending_approval(message.agent_name)
205
+ await self.team.post_tool_execution_approval(
206
+ agent_name=message.agent_name,
207
+ tool_invocation_id=message.invocation_id,
208
+ is_approved=message.is_approved,
209
+ reason=message.reason,
210
+ )
@@ -0,0 +1,180 @@
1
+ """
2
+ Defines a centralized state store for the TUI application, following state management best practices.
3
+ """
4
+ import logging
5
+ from typing import Dict, List, Optional, Any
6
+ import copy
7
+
8
+ from autobyteus.agent.context import AgentConfig
9
+ from autobyteus.agent_team.agent_team import AgentTeam
10
+ from autobyteus.agent.phases import AgentOperationalPhase
11
+ from autobyteus.agent_team.phases import AgentTeamOperationalPhase
12
+ from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
13
+ from autobyteus.agent.streaming.stream_event_payloads import (
14
+ AgentOperationalPhaseTransitionData, ToolInvocationApprovalRequestedData,
15
+ AssistantChunkData, AssistantCompleteResponseData
16
+ )
17
+ from autobyteus.agent_team.streaming.agent_team_stream_events import AgentTeamStreamEvent
18
+ from autobyteus.agent_team.streaming.agent_team_stream_event_payloads import AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload, AgentTeamPhaseTransitionData
19
+ from autobyteus.task_management.task_plan import Task
20
+ from autobyteus.task_management.events import TaskPlanPublishedEvent, TaskStatusUpdatedEvent
21
+ from autobyteus.task_management.base_task_board import TaskStatus
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class TUIStateStore:
26
+ """
27
+ A centralized store for all TUI-related state.
28
+
29
+ This class acts as the single source of truth for the UI. It processes events
30
+ from the backend and updates its state. The main App class can then react to
31
+ these state changes to update the UI components declaratively.
32
+ """
33
+
34
+ def __init__(self, team: AgentTeam):
35
+ self.team_name = team.name
36
+ self.team_role = team.role
37
+
38
+ self.focused_node_data: Optional[Dict[str, Any]] = None
39
+
40
+ self._node_roles: Dict[str, str] = self._extract_node_roles(team)
41
+ self._nodes: Dict[str, Any] = self._initialize_root_node()
42
+ self._agent_phases: Dict[str, AgentOperationalPhase] = {}
43
+ self._team_phases: Dict[str, AgentTeamOperationalPhase] = {self.team_name: AgentTeamOperationalPhase.UNINITIALIZED}
44
+ self._agent_event_history: Dict[str, List[AgentStreamEvent]] = {}
45
+ self._team_event_history: Dict[str, List[AgentTeamStreamEvent]] = {self.team_name: []}
46
+ self._pending_approvals: Dict[str, ToolInvocationApprovalRequestedData] = {}
47
+ self._speaking_agents: Dict[str, bool] = {}
48
+
49
+ # State for task boards
50
+ self._task_plans: Dict[str, List[Task]] = {} # team_name -> List[Task]
51
+ self._task_statuses: Dict[str, Dict[str, TaskStatus]] = {} # team_name -> {task_id: status}
52
+
53
+ # Version counter to signal state changes to the UI
54
+ self.version = 0
55
+
56
+ def _extract_node_roles(self, team: AgentTeam) -> Dict[str, str]:
57
+ roles = {}
58
+ if team._runtime and team._runtime.context and team._runtime.context.config:
59
+ for node_config in team._runtime.context.config.nodes:
60
+ role = getattr(node_config.node_definition, 'role', None)
61
+ if role:
62
+ roles[node_config.name] = role
63
+ return roles
64
+
65
+ def _initialize_root_node(self) -> Dict[str, Any]:
66
+ return {
67
+ self.team_name: {
68
+ "type": "team",
69
+ "name": self.team_name,
70
+ "role": self.team_role,
71
+ "children": {}
72
+ }
73
+ }
74
+
75
+ def process_event(self, event: AgentTeamStreamEvent):
76
+ self.version += 1 # Increment on any event to signal a change
77
+
78
+ if event.event_source_type == "TEAM" and isinstance(event.data, AgentTeamPhaseTransitionData):
79
+ self._team_phases[self.team_name] = event.data.new_phase
80
+
81
+ self._process_event_recursively(event, self.team_name)
82
+
83
+ def _process_event_recursively(self, event: AgentTeamStreamEvent, parent_name: str):
84
+ if parent_name not in self._team_event_history:
85
+ self._team_event_history[parent_name] = []
86
+ self._team_event_history[parent_name].append(event)
87
+
88
+ if event.event_source_type == "TASK_BOARD":
89
+ # The 'parent_name' argument holds the friendly name of the team (or sub-team)
90
+ # that is the context for this event. This is the key we use for UI state.
91
+ team_name_key = parent_name
92
+ if isinstance(event.data, TaskPlanPublishedEvent):
93
+ self._task_plans[team_name_key] = event.data.plan.tasks
94
+ # Reset statuses when a new plan is published
95
+ self._task_statuses[team_name_key] = {task.task_id: TaskStatus.NOT_STARTED for task in event.data.plan.tasks}
96
+ logger.debug(f"TUI State: Updated task plan for '{team_name_key}' with {len(event.data.plan.tasks)} tasks.")
97
+ elif isinstance(event.data, TaskStatusUpdatedEvent):
98
+ # Update status
99
+ if team_name_key not in self._task_statuses:
100
+ self._task_statuses[team_name_key] = {}
101
+ self._task_statuses[team_name_key][event.data.task_id] = event.data.new_status
102
+ logger.debug(f"TUI State: Updated status for task '{event.data.task_id}' in team '{team_name_key}' to {event.data.new_status}.")
103
+
104
+ # Update deliverables if they are provided in the event.
105
+ if event.data.deliverables is not None:
106
+ if team_name_key in self._task_plans:
107
+ for task in self._task_plans[team_name_key]:
108
+ if task.task_id == event.data.task_id:
109
+ task.file_deliverables = event.data.deliverables
110
+ logger.debug(f"TUI State: Synced deliverables for task '{event.data.task_id}' in team '{team_name_key}'.")
111
+ break
112
+ return
113
+
114
+ if isinstance(event.data, AgentEventRebroadcastPayload):
115
+ payload = event.data
116
+ agent_name = payload.agent_name
117
+ agent_event = payload.agent_event
118
+
119
+ if agent_name not in self._agent_event_history:
120
+ self._agent_event_history[agent_name] = []
121
+ if self._find_node(parent_name):
122
+ agent_role = self._node_roles.get(agent_name, "Agent")
123
+ self._add_node(agent_name, {"type": "agent", "name": agent_name, "role": agent_role, "children": {}}, parent_name)
124
+ else: logger.error(f"Cannot add agent node '{agent_name}': parent '{parent_name}' not found.")
125
+ self._agent_event_history[agent_name].append(agent_event)
126
+
127
+ if agent_event.event_type == AgentStreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION:
128
+ self._agent_phases[agent_name] = agent_event.data.new_phase
129
+ if agent_name in self._pending_approvals: del self._pending_approvals[agent_name]
130
+ elif agent_event.event_type == AgentStreamEventType.AGENT_IDLE:
131
+ self._agent_phases[agent_name] = AgentOperationalPhase.IDLE
132
+ elif agent_event.event_type == AgentStreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED:
133
+ self._pending_approvals[agent_name] = agent_event.data
134
+
135
+ elif isinstance(event.data, SubTeamEventRebroadcastPayload):
136
+ payload = event.data
137
+ sub_team_name = payload.sub_team_node_name
138
+ sub_team_event = payload.sub_team_event
139
+ if not self._find_node(sub_team_name):
140
+ role = self._node_roles.get(sub_team_name, "Sub-Team")
141
+ self._add_node(sub_team_name, {"type": "subteam", "name": sub_team_name, "role": role, "children": {}}, parent_name)
142
+ if sub_team_event.event_source_type == "TEAM" and isinstance(sub_team_event.data, AgentTeamPhaseTransitionData):
143
+ self._team_phases[sub_team_name] = sub_team_event.data.new_phase
144
+ self._process_event_recursively(sub_team_event, parent_name=sub_team_name)
145
+
146
+ def _add_node(self, node_name: str, node_data: Dict, parent_name: str):
147
+ parent = self._find_node(parent_name)
148
+ if parent: parent["children"][node_name] = node_data
149
+ else: logger.error(f"Could not find parent node '{parent_name}' to add child '{node_name}'.")
150
+
151
+ def _find_node(self, node_name: str, tree: Optional[Dict] = None) -> Optional[Dict]:
152
+ tree = tree or self._nodes
153
+ for name, node_data in tree.items():
154
+ if name == node_name: return node_data
155
+ if node_data.get("children"):
156
+ found = self._find_node(node_name, node_data.get("children"))
157
+ if found: return found
158
+ return None
159
+
160
+ def get_tree_data(self) -> Dict:
161
+ return copy.deepcopy(self._nodes)
162
+
163
+ def get_history_for_node(self, node_name: str, node_type: str) -> List:
164
+ if node_type == 'agent': return self._agent_event_history.get(node_name, [])
165
+ return []
166
+
167
+ def get_pending_approval_for_agent(self, agent_name: str) -> Optional[ToolInvocationApprovalRequestedData]:
168
+ return self._pending_approvals.get(agent_name)
169
+
170
+ def get_task_board_plan(self, team_name: str) -> Optional[List[Task]]:
171
+ return self._task_plans.get(team_name)
172
+
173
+ def get_task_board_statuses(self, team_name: str) -> Optional[Dict[str, TaskStatus]]:
174
+ return self._task_statuses.get(team_name)
175
+
176
+ def clear_pending_approval(self, agent_name: str):
177
+ if agent_name in self._pending_approvals: del self._pending_approvals[agent_name]
178
+
179
+ def set_focused_node(self, node_data: Optional[Dict[str, Any]]):
180
+ self.focused_node_data = node_data
@@ -0,0 +1,6 @@
1
+ # file: autobyteus/autobyteus/cli/agent_team_tui/widgets/__init__.py
2
+ """
3
+ Custom Textual widgets for the agent team TUI.
4
+ """
5
+ from . import renderables
6
+ from .logo import Logo