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.
- autobyteus/agent/context/__init__.py +4 -2
- autobyteus/agent/context/agent_config.py +0 -4
- autobyteus/agent/context/agent_context_registry.py +73 -0
- autobyteus/agent/events/notifiers.py +4 -0
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +7 -2
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
- autobyteus/agent/handlers/user_input_message_event_handler.py +15 -0
- autobyteus/agent/message/send_message_to.py +29 -23
- autobyteus/agent/runtime/agent_runtime.py +10 -2
- autobyteus/agent/sender_type.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +6 -0
- autobyteus/agent/streaming/stream_event_payloads.py +12 -0
- autobyteus/agent/streaming/stream_events.py +3 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
- autobyteus/agent_team/__init__.py +1 -0
- autobyteus/agent_team/agent_team.py +93 -0
- autobyteus/agent_team/agent_team_builder.py +184 -0
- autobyteus/agent_team/base_agent_team.py +86 -0
- autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
- autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
- autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
- autobyteus/agent_team/context/__init__.py +17 -0
- autobyteus/agent_team/context/agent_team_config.py +33 -0
- autobyteus/agent_team/context/agent_team_context.py +61 -0
- autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
- autobyteus/agent_team/context/team_manager.py +147 -0
- autobyteus/agent_team/context/team_node_config.py +76 -0
- autobyteus/agent_team/events/__init__.py +29 -0
- autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
- autobyteus/agent_team/events/agent_team_events.py +53 -0
- autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
- autobyteus/agent_team/exceptions.py +8 -0
- autobyteus/agent_team/factory/__init__.py +9 -0
- autobyteus/agent_team/factory/agent_team_factory.py +99 -0
- autobyteus/agent_team/handlers/__init__.py +19 -0
- autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
- autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
- autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
- autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
- autobyteus/agent_team/phases/__init__.py +11 -0
- autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
- autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
- autobyteus/agent_team/runtime/__init__.py +13 -0
- autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
- autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
- autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
- autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
- autobyteus/agent_team/streaming/__init__.py +26 -0
- autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
- autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
- autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
- autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
- autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
- autobyteus/agent_team/task_notification/__init__.py +11 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
- autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
- autobyteus/agent_team/utils/__init__.py +9 -0
- autobyteus/agent_team/utils/wait_for_idle.py +46 -0
- autobyteus/cli/agent_team_tui/__init__.py +4 -0
- autobyteus/cli/agent_team_tui/app.py +210 -0
- autobyteus/cli/agent_team_tui/state.py +180 -0
- autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
- autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
- autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
- autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
- autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
- autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
- autobyteus/events/event_types.py +7 -2
- autobyteus/llm/api/autobyteus_llm.py +11 -12
- autobyteus/llm/api/lmstudio_llm.py +10 -13
- autobyteus/llm/api/ollama_llm.py +8 -13
- autobyteus/llm/autobyteus_provider.py +73 -46
- autobyteus/llm/llm_factory.py +102 -140
- autobyteus/llm/lmstudio_provider.py +63 -48
- autobyteus/llm/models.py +83 -53
- autobyteus/llm/ollama_provider.py +69 -61
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +13 -13
- autobyteus/llm/runtimes.py +11 -0
- autobyteus/task_management/__init__.py +43 -0
- autobyteus/task_management/base_task_board.py +68 -0
- autobyteus/task_management/converters/__init__.py +11 -0
- autobyteus/task_management/converters/task_board_converter.py +64 -0
- autobyteus/task_management/converters/task_plan_converter.py +48 -0
- autobyteus/task_management/deliverable.py +16 -0
- autobyteus/task_management/deliverables/__init__.py +8 -0
- autobyteus/task_management/deliverables/file_deliverable.py +15 -0
- autobyteus/task_management/events.py +27 -0
- autobyteus/task_management/in_memory_task_board.py +126 -0
- autobyteus/task_management/schemas/__init__.py +15 -0
- autobyteus/task_management/schemas/deliverable_schema.py +13 -0
- autobyteus/task_management/schemas/plan_definition.py +35 -0
- autobyteus/task_management/schemas/task_status_report.py +27 -0
- autobyteus/task_management/task_plan.py +110 -0
- autobyteus/task_management/tools/__init__.py +14 -0
- autobyteus/task_management/tools/get_task_board_status.py +68 -0
- autobyteus/task_management/tools/publish_task_plan.py +113 -0
- autobyteus/task_management/tools/update_task_status.py +135 -0
- autobyteus/tools/bash/bash_executor.py +59 -14
- autobyteus/tools/mcp/config_service.py +63 -58
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server_instance_manager.py +30 -4
- autobyteus/tools/mcp/tool_registrar.py +103 -50
- autobyteus/tools/parameter_schema.py +17 -11
- autobyteus/tools/registry/tool_definition.py +24 -29
- autobyteus/tools/tool_category.py +1 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
- autobyteus/tools/usage/providers/__init__.py +2 -12
- autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
- autobyteus/tools/usage/registries/__init__.py +7 -12
- autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
- autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
- autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/METADATA +3 -3
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/RECORD +146 -72
- examples/agent_team/__init__.py +1 -0
- examples/run_browser_agent.py +17 -15
- examples/run_google_slides_agent.py +17 -16
- examples/run_poem_writer.py +22 -12
- examples/run_sqlite_agent.py +17 -15
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
- autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
- autobyteus/tools/usage/providers/json_example_provider.py +0 -32
- autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
- examples/workflow/__init__.py +0 -1
- examples/workflow/run_basic_research_workflow.py +0 -189
- examples/workflow/run_code_review_workflow.py +0 -269
- examples/workflow/run_debate_workflow.py +0 -212
- examples/workflow/run_workflow_with_tui.py +0 -153
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.4.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {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,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,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
|