autobyteus 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autobyteus/agent/context/agent_runtime_state.py +4 -0
- autobyteus/agent/events/notifiers.py +5 -1
- autobyteus/agent/message/send_message_to.py +5 -4
- autobyteus/agent/streaming/agent_event_stream.py +5 -0
- autobyteus/agent/streaming/stream_event_payloads.py +25 -0
- autobyteus/agent/streaming/stream_events.py +13 -1
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +4 -4
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +12 -12
- autobyteus/agent_team/context/agent_team_runtime_state.py +2 -2
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +4 -4
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +3 -3
- autobyteus/agent_team/streaming/agent_team_stream_events.py +8 -8
- autobyteus/agent_team/task_notification/activation_policy.py +1 -1
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +22 -22
- autobyteus/agent_team/task_notification/task_notification_mode.py +1 -1
- autobyteus/cli/agent_team_tui/app.py +4 -4
- autobyteus/cli/agent_team_tui/state.py +8 -8
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +3 -3
- autobyteus/cli/agent_team_tui/widgets/shared.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/{task_board_panel.py → task_plan_panel.py} +5 -5
- autobyteus/events/event_types.py +4 -3
- autobyteus/multimedia/audio/api/__init__.py +3 -2
- autobyteus/multimedia/audio/api/openai_audio_client.py +112 -0
- autobyteus/multimedia/audio/audio_client_factory.py +37 -0
- autobyteus/multimedia/image/image_client_factory.py +1 -1
- autobyteus/task_management/__init__.py +43 -20
- autobyteus/task_management/{base_task_board.py → base_task_plan.py} +16 -13
- autobyteus/task_management/converters/__init__.py +2 -2
- autobyteus/task_management/converters/{task_board_converter.py → task_plan_converter.py} +13 -13
- autobyteus/task_management/events.py +7 -7
- autobyteus/task_management/{in_memory_task_board.py → in_memory_task_plan.py} +34 -22
- autobyteus/task_management/schemas/__init__.py +3 -0
- autobyteus/task_management/schemas/task_status_report.py +2 -2
- autobyteus/task_management/schemas/todo_definition.py +15 -0
- autobyteus/task_management/todo.py +29 -0
- autobyteus/task_management/todo_list.py +75 -0
- autobyteus/task_management/tools/__init__.py +24 -8
- autobyteus/task_management/tools/task_tools/__init__.py +19 -0
- autobyteus/task_management/tools/{assign_task_to.py → task_tools/assign_task_to.py} +18 -18
- autobyteus/task_management/tools/{publish_task.py → task_tools/create_task.py} +16 -18
- autobyteus/task_management/tools/{publish_tasks.py → task_tools/create_tasks.py} +19 -19
- autobyteus/task_management/tools/{get_my_tasks.py → task_tools/get_my_tasks.py} +15 -15
- autobyteus/task_management/tools/{get_task_board_status.py → task_tools/get_task_plan_status.py} +16 -16
- autobyteus/task_management/tools/{update_task_status.py → task_tools/update_task_status.py} +16 -16
- autobyteus/task_management/tools/todo_tools/__init__.py +18 -0
- autobyteus/task_management/tools/todo_tools/add_todo.py +78 -0
- autobyteus/task_management/tools/todo_tools/create_todo_list.py +79 -0
- autobyteus/task_management/tools/todo_tools/get_todo_list.py +55 -0
- autobyteus/task_management/tools/todo_tools/update_todo_status.py +85 -0
- autobyteus/tools/__init__.py +15 -11
- autobyteus/tools/bash/bash_executor.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +5 -5
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -3
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -3
- autobyteus/tools/browser/standalone/navigate_to.py +13 -9
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +9 -5
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +10 -6
- autobyteus/tools/browser/standalone/webpage_reader.py +13 -9
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +9 -5
- autobyteus/tools/file/__init__.py +13 -0
- autobyteus/tools/file/{file_editor.py → edit_file.py} +11 -11
- autobyteus/tools/file/list_directory.py +168 -0
- autobyteus/tools/file/{file_reader.py → read_file.py} +3 -3
- autobyteus/tools/file/search_files.py +188 -0
- autobyteus/tools/file/{file_writer.py → write_file.py} +3 -3
- autobyteus/tools/functional_tool.py +10 -8
- autobyteus/tools/mcp/tool.py +3 -3
- autobyteus/tools/mcp/tool_registrar.py +5 -2
- autobyteus/tools/multimedia/__init__.py +2 -1
- autobyteus/tools/multimedia/audio_tools.py +2 -2
- autobyteus/tools/{download_media_tool.py → multimedia/download_media_tool.py} +3 -3
- autobyteus/tools/multimedia/image_tools.py +4 -4
- autobyteus/tools/multimedia/media_reader_tool.py +1 -1
- autobyteus/tools/registry/tool_definition.py +66 -13
- autobyteus/tools/registry/tool_registry.py +29 -0
- autobyteus/tools/search/__init__.py +17 -0
- autobyteus/tools/search/base_strategy.py +35 -0
- autobyteus/tools/search/client.py +24 -0
- autobyteus/tools/search/factory.py +81 -0
- autobyteus/tools/search/google_cse_strategy.py +68 -0
- autobyteus/tools/search/providers.py +10 -0
- autobyteus/tools/search/serpapi_strategy.py +65 -0
- autobyteus/tools/search/serper_strategy.py +87 -0
- autobyteus/tools/search_tool.py +83 -0
- autobyteus/tools/timer.py +4 -0
- autobyteus/tools/tool_meta.py +4 -24
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +1 -2
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/METADATA +5 -5
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/RECORD +95 -80
- examples/run_agentic_software_engineer.py +239 -0
- examples/run_poem_writer.py +3 -3
- autobyteus/person/__init__.py +0 -0
- autobyteus/person/examples/__init__.py +0 -0
- autobyteus/person/examples/sample_persons.py +0 -14
- autobyteus/person/examples/sample_roles.py +0 -14
- autobyteus/person/person.py +0 -29
- autobyteus/person/role.py +0 -14
- autobyteus/tools/google_search.py +0 -149
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/WHEEL +0 -0
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.2.0.dist-info → autobyteus-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@ from autobyteus.agent.workspace.base_workspace import BaseAgentWorkspace
|
|
|
12
12
|
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
13
13
|
# LLMConfig is no longer needed here
|
|
14
14
|
# from autobyteus.llm.utils.llm_config import LLMConfig
|
|
15
|
+
from autobyteus.task_management.todo_list import ToDoList
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from autobyteus.agent.phases import AgentPhaseManager
|
|
@@ -52,6 +53,9 @@ class AgentRuntimeState:
|
|
|
52
53
|
# NEW: State for multi-tool call invocation turns, with a very explicit name.
|
|
53
54
|
self.active_multi_tool_call_turn: Optional['ToolInvocationTurn'] = None
|
|
54
55
|
|
|
56
|
+
# NEW: State for the agent's personal ToDoList
|
|
57
|
+
self.todo_list: Optional[ToDoList] = None
|
|
58
|
+
|
|
55
59
|
self.processed_system_prompt: Optional[str] = None
|
|
56
60
|
# self.final_llm_config_for_creation removed
|
|
57
61
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/agent/events/notifiers.py
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
3
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING, List
|
|
4
4
|
|
|
5
5
|
from autobyteus.events.event_emitter import EventEmitter
|
|
6
6
|
from autobyteus.events.event_types import EventType
|
|
@@ -117,6 +117,10 @@ class AgentExternalEventNotifier(EventEmitter):
|
|
|
117
117
|
"""Notifies that the agent has received a system-generated task notification."""
|
|
118
118
|
self._emit_event(EventType.AGENT_DATA_SYSTEM_TASK_NOTIFICATION_RECEIVED, payload_content=notification_data)
|
|
119
119
|
|
|
120
|
+
def notify_agent_data_todo_list_updated(self, todo_list: List[Dict[str, Any]]):
|
|
121
|
+
"""Notifies that the agent's ToDo list has been updated."""
|
|
122
|
+
self._emit_event(EventType.AGENT_DATA_TODO_LIST_UPDATED, payload_content={"todos": todo_list})
|
|
123
|
+
|
|
120
124
|
def notify_agent_error_output_generation(self, error_source: str, error_message: str, error_details: Optional[str] = None):
|
|
121
125
|
payload_dict = {
|
|
122
126
|
"source": error_source,
|
|
@@ -21,7 +21,7 @@ class SendMessageTo(BaseTool):
|
|
|
21
21
|
This tool dynamically retrieves the team communication channel from the
|
|
22
22
|
agent's context at runtime.
|
|
23
23
|
"""
|
|
24
|
-
TOOL_NAME = "
|
|
24
|
+
TOOL_NAME = "send_message_to"
|
|
25
25
|
CATEGORY = ToolCategory.AGENT_COMMUNICATION
|
|
26
26
|
|
|
27
27
|
def __init__(self, config: Optional[ToolConfig] = None):
|
|
@@ -30,7 +30,7 @@ class SendMessageTo(BaseTool):
|
|
|
30
30
|
"""
|
|
31
31
|
super().__init__(config=config)
|
|
32
32
|
# The TeamManager is no longer stored as an instance variable.
|
|
33
|
-
logger.debug("
|
|
33
|
+
logger.debug("%s tool initialized (stateless).", self.get_name())
|
|
34
34
|
|
|
35
35
|
# The set_team_manager method has been removed.
|
|
36
36
|
|
|
@@ -81,7 +81,8 @@ class SendMessageTo(BaseTool):
|
|
|
81
81
|
# --- NEW: Retrieve TeamManager dynamically from context ---
|
|
82
82
|
team_context: Optional['AgentTeamContext'] = context.custom_data.get("team_context")
|
|
83
83
|
if not team_context:
|
|
84
|
-
error_msg = "Critical error:
|
|
84
|
+
error_msg = (f"Critical error: {self.get_name()} tool is not configured for team communication. "
|
|
85
|
+
"It can only be used within a managed AgentTeam.")
|
|
85
86
|
logger.error(f"Agent '{context.agent_id}': {error_msg}")
|
|
86
87
|
return f"Error: {error_msg}"
|
|
87
88
|
|
|
@@ -122,4 +123,4 @@ class SendMessageTo(BaseTool):
|
|
|
122
123
|
|
|
123
124
|
success_msg = f"Message dispatch for recipient '{recipient_name}' has been successfully requested."
|
|
124
125
|
logger.info(f"Tool '{self.get_name()}': {success_msg}")
|
|
125
|
-
return success_msg
|
|
126
|
+
return success_msg
|
|
@@ -17,6 +17,7 @@ from autobyteus.agent.streaming.stream_event_payloads import (
|
|
|
17
17
|
create_tool_invocation_approval_requested_data,
|
|
18
18
|
create_tool_invocation_auto_executing_data,
|
|
19
19
|
create_system_task_notification_data, # NEW
|
|
20
|
+
create_todo_list_update_data,
|
|
20
21
|
AssistantChunkData,
|
|
21
22
|
AssistantCompleteResponseData,
|
|
22
23
|
ToolInteractionLogEntryData,
|
|
@@ -25,6 +26,7 @@ from autobyteus.agent.streaming.stream_event_payloads import (
|
|
|
25
26
|
ToolInvocationAutoExecutingData,
|
|
26
27
|
ErrorEventData,
|
|
27
28
|
SystemTaskNotificationData, # NEW
|
|
29
|
+
ToDoListUpdateData,
|
|
28
30
|
EmptyData,
|
|
29
31
|
StreamDataPayload,
|
|
30
32
|
)
|
|
@@ -107,6 +109,9 @@ class AgentEventStream(EventEmitter):
|
|
|
107
109
|
elif event_type == EventType.AGENT_DATA_SYSTEM_TASK_NOTIFICATION_RECEIVED:
|
|
108
110
|
typed_payload_for_stream_event = create_system_task_notification_data(payload)
|
|
109
111
|
stream_event_type_for_generic_stream = StreamEventType.SYSTEM_TASK_NOTIFICATION
|
|
112
|
+
elif event_type == EventType.AGENT_DATA_TODO_LIST_UPDATED:
|
|
113
|
+
typed_payload_for_stream_event = create_todo_list_update_data(payload)
|
|
114
|
+
stream_event_type_for_generic_stream = StreamEventType.AGENT_TODO_LIST_UPDATE
|
|
110
115
|
|
|
111
116
|
elif event_type in [EventType.AGENT_DATA_ASSISTANT_CHUNK_STREAM_END, EventType.AGENT_DATA_TOOL_LOG_STREAM_END]:
|
|
112
117
|
pass
|
|
@@ -66,6 +66,14 @@ class SystemTaskNotificationData(BaseStreamPayload):
|
|
|
66
66
|
sender_id: str
|
|
67
67
|
content: str
|
|
68
68
|
|
|
69
|
+
class ToDoItemData(BaseStreamPayload):
|
|
70
|
+
description: str
|
|
71
|
+
todo_id: str
|
|
72
|
+
status: str
|
|
73
|
+
|
|
74
|
+
class ToDoListUpdateData(BaseStreamPayload):
|
|
75
|
+
todos: List[ToDoItemData]
|
|
76
|
+
|
|
69
77
|
class EmptyData(BaseStreamPayload):
|
|
70
78
|
pass
|
|
71
79
|
|
|
@@ -79,6 +87,7 @@ StreamDataPayload = Union[
|
|
|
79
87
|
ToolInvocationApprovalRequestedData,
|
|
80
88
|
ToolInvocationAutoExecutingData,
|
|
81
89
|
SystemTaskNotificationData, # NEW
|
|
90
|
+
ToDoListUpdateData,
|
|
82
91
|
EmptyData
|
|
83
92
|
]
|
|
84
93
|
|
|
@@ -196,3 +205,19 @@ def create_system_task_notification_data(notification_data_dict: Any) -> SystemT
|
|
|
196
205
|
return SystemTaskNotificationData(**notification_data_dict)
|
|
197
206
|
raise ValueError(f"Cannot create SystemTaskNotificationData from {type(notification_data_dict)}")
|
|
198
207
|
|
|
208
|
+
def create_todo_list_update_data(todo_data_dict: Any) -> ToDoListUpdateData:
|
|
209
|
+
if isinstance(todo_data_dict, dict):
|
|
210
|
+
todos_payload = todo_data_dict.get('todos', [])
|
|
211
|
+
if not isinstance(todos_payload, list):
|
|
212
|
+
raise ValueError("Expected 'todos' to be a list when creating ToDoListUpdateData.")
|
|
213
|
+
todo_items = []
|
|
214
|
+
for todo_entry in todos_payload:
|
|
215
|
+
if not isinstance(todo_entry, dict):
|
|
216
|
+
logger.warning(f"Skipping non-dict todo entry when creating ToDoListUpdateData: {todo_entry!r}")
|
|
217
|
+
continue
|
|
218
|
+
try:
|
|
219
|
+
todo_items.append(ToDoItemData(**todo_entry))
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logger.warning(f"Failed to parse todo entry into ToDoItemData: {todo_entry!r}; error: {exc}")
|
|
222
|
+
return ToDoListUpdateData(todos=todo_items)
|
|
223
|
+
raise ValueError(f"Cannot create ToDoListUpdateData from {type(todo_data_dict)}")
|
|
@@ -17,7 +17,17 @@ from .stream_event_payloads import (
|
|
|
17
17
|
ToolInvocationApprovalRequestedData,
|
|
18
18
|
ToolInvocationAutoExecutingData,
|
|
19
19
|
SystemTaskNotificationData, # NEW
|
|
20
|
-
|
|
20
|
+
ToDoListUpdateData,
|
|
21
|
+
EmptyData,
|
|
22
|
+
create_assistant_chunk_data,
|
|
23
|
+
create_assistant_complete_response_data,
|
|
24
|
+
create_tool_interaction_log_entry_data,
|
|
25
|
+
create_agent_operational_phase_transition_data,
|
|
26
|
+
create_error_event_data,
|
|
27
|
+
create_tool_invocation_approval_requested_data,
|
|
28
|
+
create_tool_invocation_auto_executing_data,
|
|
29
|
+
create_system_task_notification_data, # NEW
|
|
30
|
+
create_todo_list_update_data,
|
|
21
31
|
)
|
|
22
32
|
|
|
23
33
|
logger = logging.getLogger(__name__)
|
|
@@ -35,6 +45,7 @@ class StreamEventType(str, Enum):
|
|
|
35
45
|
TOOL_INVOCATION_APPROVAL_REQUESTED = "tool_invocation_approval_requested"
|
|
36
46
|
TOOL_INVOCATION_AUTO_EXECUTING = "tool_invocation_auto_executing"
|
|
37
47
|
SYSTEM_TASK_NOTIFICATION = "system_task_notification" # NEW
|
|
48
|
+
AGENT_TODO_LIST_UPDATE = "agent_todo_list_updated"
|
|
38
49
|
AGENT_IDLE = "agent_idle"
|
|
39
50
|
|
|
40
51
|
|
|
@@ -47,6 +58,7 @@ _STREAM_EVENT_TYPE_TO_PAYLOAD_CLASS: Dict[StreamEventType, Type[BaseModel]] = {
|
|
|
47
58
|
StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED: ToolInvocationApprovalRequestedData,
|
|
48
59
|
StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING: ToolInvocationAutoExecutingData,
|
|
49
60
|
StreamEventType.SYSTEM_TASK_NOTIFICATION: SystemTaskNotificationData, # NEW
|
|
61
|
+
StreamEventType.AGENT_TODO_LIST_UPDATE: ToDoListUpdateData,
|
|
50
62
|
StreamEventType.AGENT_IDLE: AgentOperationalPhaseTransitionData,
|
|
51
63
|
}
|
|
52
64
|
|
|
@@ -27,9 +27,9 @@ class TaskNotifierInitializationStep(BaseAgentTeamBootstrapStep):
|
|
|
27
27
|
|
|
28
28
|
logger.info(f"Team '{team_id}': Mode is SYSTEM_EVENT_DRIVEN. Initializing and activating task notifier.")
|
|
29
29
|
try:
|
|
30
|
-
|
|
31
|
-
if not
|
|
32
|
-
logger.error(f"Team '{team_id}':
|
|
30
|
+
task_plan = context.state.task_plan
|
|
31
|
+
if not task_plan:
|
|
32
|
+
logger.error(f"Team '{team_id}': TaskPlan not found. Cannot initialize task notifier. This step should run after TeamContextInitializationStep.")
|
|
33
33
|
return False
|
|
34
34
|
|
|
35
35
|
team_manager = context.team_manager
|
|
@@ -38,7 +38,7 @@ class TaskNotifierInitializationStep(BaseAgentTeamBootstrapStep):
|
|
|
38
38
|
return False
|
|
39
39
|
|
|
40
40
|
notifier = SystemEventDrivenAgentTaskNotifier(
|
|
41
|
-
|
|
41
|
+
task_plan=task_plan,
|
|
42
42
|
team_manager=team_manager
|
|
43
43
|
)
|
|
44
44
|
notifier.start_monitoring()
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from autobyteus.agent_team.bootstrap_steps.base_agent_team_bootstrap_step import BaseAgentTeamBootstrapStep
|
|
6
|
-
from autobyteus.task_management import
|
|
6
|
+
from autobyteus.task_management import TaskPlan
|
|
7
7
|
from autobyteus.events.event_types import EventType
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
@@ -15,29 +15,29 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
class TeamContextInitializationStep(BaseAgentTeamBootstrapStep):
|
|
16
16
|
"""
|
|
17
17
|
Bootstrap step to initialize shared team context components, such as the
|
|
18
|
-
|
|
18
|
+
TaskPlan, and bridges its events to the team's notifier.
|
|
19
19
|
"""
|
|
20
20
|
async def execute(self, context: 'AgentTeamContext', phase_manager: 'AgentTeamPhaseManager') -> bool:
|
|
21
21
|
team_id = context.team_id
|
|
22
22
|
logger.info(f"Team '{team_id}': Executing TeamContextInitializationStep.")
|
|
23
23
|
try:
|
|
24
|
-
if context.state.
|
|
25
|
-
|
|
26
|
-
context.state.
|
|
27
|
-
logger.info(f"Team '{team_id}':
|
|
24
|
+
if context.state.task_plan is None:
|
|
25
|
+
task_plan = TaskPlan(team_id=team_id)
|
|
26
|
+
context.state.task_plan = task_plan
|
|
27
|
+
logger.info(f"Team '{team_id}': TaskPlan initialized and attached to team state.")
|
|
28
28
|
|
|
29
29
|
notifier = phase_manager.notifier
|
|
30
30
|
if notifier:
|
|
31
31
|
# The notifier, a long-lived component, subscribes to events
|
|
32
|
-
# from the
|
|
33
|
-
notifier.subscribe_from(sender=
|
|
34
|
-
notifier.subscribe_from(sender=
|
|
35
|
-
logger.info(f"Team '{team_id}': Successfully bridged
|
|
32
|
+
# from the task_plan, another long-lived component.
|
|
33
|
+
notifier.subscribe_from(sender=task_plan, event=EventType.TASK_PLAN_TASKS_CREATED, listener=notifier.handle_and_publish_task_plan_event)
|
|
34
|
+
notifier.subscribe_from(sender=task_plan, event=EventType.TASK_PLAN_STATUS_UPDATED, listener=notifier.handle_and_publish_task_plan_event)
|
|
35
|
+
logger.info(f"Team '{team_id}': Successfully bridged TaskPlan events to the team notifier.")
|
|
36
36
|
else:
|
|
37
|
-
logger.warning(f"Team '{team_id}': Notifier not found in PhaseManager. Cannot bridge
|
|
37
|
+
logger.warning(f"Team '{team_id}': Notifier not found in PhaseManager. Cannot bridge TaskPlan events.")
|
|
38
38
|
|
|
39
39
|
else:
|
|
40
|
-
logger.warning(f"Team '{team_id}':
|
|
40
|
+
logger.warning(f"Team '{team_id}': TaskPlan already exists. Skipping initialization.")
|
|
41
41
|
|
|
42
42
|
return True
|
|
43
43
|
except Exception as e:
|
|
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from autobyteus.agent_team.context.team_node_config import TeamNodeConfig
|
|
14
14
|
from autobyteus.agent_team.context.team_manager import TeamManager
|
|
15
15
|
from autobyteus.agent_team.streaming.agent_event_multiplexer import AgentEventMultiplexer
|
|
16
|
-
from autobyteus.task_management.
|
|
16
|
+
from autobyteus.task_management.base_task_plan import BaseTaskPlan
|
|
17
17
|
from autobyteus.agent_team.task_notification.system_event_driven_agent_task_notifier import SystemEventDrivenAgentTaskNotifier
|
|
18
18
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
@@ -38,7 +38,7 @@ class AgentTeamRuntimeState:
|
|
|
38
38
|
multiplexer_ref: Optional['AgentEventMultiplexer'] = None
|
|
39
39
|
|
|
40
40
|
# Dynamic planning and artifact state
|
|
41
|
-
|
|
41
|
+
task_plan: Optional['BaseTaskPlan'] = None
|
|
42
42
|
|
|
43
43
|
def __post_init__(self):
|
|
44
44
|
if not self.team_id or not isinstance(self.team_id, str):
|
|
@@ -7,7 +7,7 @@ from autobyteus.events.event_types import EventType
|
|
|
7
7
|
from autobyteus.agent_team.phases.agent_team_operational_phase import AgentTeamOperationalPhase
|
|
8
8
|
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent
|
|
9
9
|
from .agent_team_stream_events import AgentTeamStreamEvent, AgentEventRebroadcastPayload, AgentTeamPhaseTransitionData, SubTeamEventRebroadcastPayload
|
|
10
|
-
from autobyteus.task_management.events import
|
|
10
|
+
from autobyteus.task_management.events import BaseTaskPlanEvent
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from autobyteus.agent_team.runtime.agent_team_runtime import AgentTeamRuntime
|
|
@@ -56,9 +56,9 @@ class AgentTeamExternalEventNotifier(EventEmitter):
|
|
|
56
56
|
event = AgentTeamStreamEvent(team_id=self.team_id, event_source_type="SUB_TEAM", data=SubTeamEventRebroadcastPayload(sub_team_node_name=sub_team_node_name, sub_team_event=sub_team_event))
|
|
57
57
|
self._emit_event(event)
|
|
58
58
|
|
|
59
|
-
def
|
|
59
|
+
def handle_and_publish_task_plan_event(self, payload: BaseTaskPlanEvent, **kwargs):
|
|
60
60
|
"""
|
|
61
|
-
Listener for
|
|
61
|
+
Listener for TaskPlan events. It wraps the event and publishes it on the main team stream.
|
|
62
62
|
"""
|
|
63
|
-
event = AgentTeamStreamEvent(team_id=self.team_id, event_source_type="
|
|
63
|
+
event = AgentTeamStreamEvent(team_id=self.team_id, event_source_type="TASK_PLAN", data=payload)
|
|
64
64
|
self._emit_event(event)
|
|
@@ -3,7 +3,7 @@ from typing import Optional, Any
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
from autobyteus.agent_team.phases.agent_team_operational_phase import AgentTeamOperationalPhase
|
|
5
5
|
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent
|
|
6
|
-
from autobyteus.task_management.events import
|
|
6
|
+
from autobyteus.task_management.events import TasksCreatedEvent, TaskStatusUpdatedEvent
|
|
7
7
|
# Need to use a forward reference string to avoid circular import at runtime
|
|
8
8
|
from typing import TYPE_CHECKING, Union
|
|
9
9
|
if TYPE_CHECKING:
|
|
@@ -28,5 +28,5 @@ class SubTeamEventRebroadcastPayload(BaseModel):
|
|
|
28
28
|
sub_team_node_name: str # The friendly name of the sub-team node
|
|
29
29
|
sub_team_event: "AgentTeamStreamEvent" = Field(..., description="The original, unmodified event from the sub-team's stream")
|
|
30
30
|
|
|
31
|
-
# --- Payload for events originating from the "
|
|
32
|
-
|
|
31
|
+
# --- Payload for events originating from the "TASK_PLAN" source ---
|
|
32
|
+
TaskPlanEventPayload = Union[TasksCreatedEvent, TaskStatusUpdatedEvent]
|
|
@@ -6,21 +6,21 @@ from pydantic import BaseModel, Field, model_validator
|
|
|
6
6
|
|
|
7
7
|
from .agent_team_stream_event_payloads import (
|
|
8
8
|
AgentTeamPhaseTransitionData, AgentEventRebroadcastPayload,
|
|
9
|
-
SubTeamEventRebroadcastPayload,
|
|
9
|
+
SubTeamEventRebroadcastPayload, TaskPlanEventPayload
|
|
10
10
|
)
|
|
11
|
-
from autobyteus.task_management.events import
|
|
11
|
+
from autobyteus.task_management.events import BaseTaskPlanEvent
|
|
12
12
|
|
|
13
13
|
# A union of all possible payloads for a "TEAM" sourced event.
|
|
14
14
|
TeamSpecificPayload = Union[AgentTeamPhaseTransitionData]
|
|
15
15
|
|
|
16
16
|
# The top-level discriminated union for the main event stream's payload.
|
|
17
|
-
AgentTeamStreamDataPayload = Union[TeamSpecificPayload, AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload,
|
|
17
|
+
AgentTeamStreamDataPayload = Union[TeamSpecificPayload, AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload, TaskPlanEventPayload]
|
|
18
18
|
|
|
19
19
|
class AgentTeamStreamEvent(BaseModel):
|
|
20
20
|
event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
21
21
|
timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
|
|
22
22
|
team_id: str
|
|
23
|
-
event_source_type: Literal["TEAM", "AGENT", "SUB_TEAM", "
|
|
23
|
+
event_source_type: Literal["TEAM", "AGENT", "SUB_TEAM", "TASK_PLAN"]
|
|
24
24
|
data: AgentTeamStreamDataPayload
|
|
25
25
|
|
|
26
26
|
@model_validator(mode='after')
|
|
@@ -34,8 +34,8 @@ class AgentTeamStreamEvent(BaseModel):
|
|
|
34
34
|
is_team_event = self.event_source_type == "TEAM"
|
|
35
35
|
is_team_payload = isinstance(self.data, AgentTeamPhaseTransitionData)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
is_task_plan_event = self.event_source_type == "TASK_PLAN"
|
|
38
|
+
is_task_plan_payload = isinstance(self.data, BaseTaskPlanEvent)
|
|
39
39
|
|
|
40
40
|
if is_agent_event and not is_agent_payload:
|
|
41
41
|
raise ValueError("event_source_type is 'AGENT' but data is not an AgentEventRebroadcastPayload")
|
|
@@ -46,8 +46,8 @@ class AgentTeamStreamEvent(BaseModel):
|
|
|
46
46
|
if is_team_event and not is_team_payload:
|
|
47
47
|
raise ValueError("event_source_type is 'TEAM' but data is not a valid team-specific payload")
|
|
48
48
|
|
|
49
|
-
if
|
|
50
|
-
raise ValueError("event_source_type is '
|
|
49
|
+
if is_task_plan_event and not is_task_plan_payload:
|
|
50
|
+
raise ValueError("event_source_type is 'TASK_PLAN' but data is not a BaseTaskPlanEvent instance")
|
|
51
51
|
|
|
52
52
|
return self
|
|
53
53
|
|
|
@@ -31,7 +31,7 @@ class ActivationPolicy:
|
|
|
31
31
|
def reset(self):
|
|
32
32
|
"""
|
|
33
33
|
Resets the activation state. This should be called when a new batch of
|
|
34
|
-
tasks is published to the task
|
|
34
|
+
tasks is published to the task plan, signifying a new plan or a
|
|
35
35
|
significant change in scope.
|
|
36
36
|
"""
|
|
37
37
|
logger.info(f"Team '{self._team_id}': ActivationPolicy state has been reset. All agents are now considered inactive.")
|
|
@@ -3,39 +3,39 @@ import logging
|
|
|
3
3
|
from typing import Union, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from autobyteus.events.event_types import EventType
|
|
6
|
-
from autobyteus.task_management.events import
|
|
7
|
-
from autobyteus.task_management.
|
|
6
|
+
from autobyteus.task_management.events import TasksCreatedEvent, TaskStatusUpdatedEvent
|
|
7
|
+
from autobyteus.task_management.base_task_plan import TaskStatus
|
|
8
8
|
|
|
9
9
|
# Import the new, separated components
|
|
10
10
|
from .activation_policy import ActivationPolicy
|
|
11
11
|
from .task_activator import TaskActivator
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
-
from autobyteus.task_management.
|
|
14
|
+
from autobyteus.task_management.base_task_plan import BaseTaskPlan
|
|
15
15
|
from autobyteus.agent_team.context.team_manager import TeamManager
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
class SystemEventDrivenAgentTaskNotifier:
|
|
20
20
|
"""
|
|
21
|
-
An internal component that monitors a
|
|
21
|
+
An internal component that monitors a TaskPlan and orchestrates agent
|
|
22
22
|
activation based on task runnability.
|
|
23
23
|
|
|
24
24
|
This class acts as a conductor, delegating the logic for *when* to activate
|
|
25
25
|
to an ActivationPolicy and the action of *how* to activate to a TaskActivator.
|
|
26
26
|
"""
|
|
27
|
-
def __init__(self,
|
|
27
|
+
def __init__(self, task_plan: 'BaseTaskPlan', team_manager: 'TeamManager'):
|
|
28
28
|
"""
|
|
29
29
|
Initializes the SystemEventDrivenAgentTaskNotifier.
|
|
30
30
|
|
|
31
31
|
Args:
|
|
32
|
-
|
|
32
|
+
task_plan: The team's shared task plan instance.
|
|
33
33
|
team_manager: The team's manager for activating agents.
|
|
34
34
|
"""
|
|
35
|
-
if not
|
|
36
|
-
raise ValueError("
|
|
35
|
+
if not task_plan or not team_manager:
|
|
36
|
+
raise ValueError("TaskPlan and TeamManager are required for the notifier.")
|
|
37
37
|
|
|
38
|
-
self.
|
|
38
|
+
self._task_plan = task_plan
|
|
39
39
|
self._team_manager = team_manager
|
|
40
40
|
|
|
41
41
|
# Instantiate the components that hold the actual logic and action
|
|
@@ -46,27 +46,27 @@ class SystemEventDrivenAgentTaskNotifier:
|
|
|
46
46
|
|
|
47
47
|
def start_monitoring(self):
|
|
48
48
|
"""
|
|
49
|
-
Subscribes to task
|
|
49
|
+
Subscribes to task plan events to begin monitoring for runnable tasks.
|
|
50
50
|
"""
|
|
51
|
-
self.
|
|
52
|
-
self.
|
|
53
|
-
logger.info(f"Team '{self._team_manager.team_id}': Task notifier orchestrator is now monitoring
|
|
51
|
+
self._task_plan.subscribe(EventType.TASK_PLAN_TASKS_CREATED, self._handle_tasks_changed)
|
|
52
|
+
self._task_plan.subscribe(EventType.TASK_PLAN_STATUS_UPDATED, self._handle_tasks_changed)
|
|
53
|
+
logger.info(f"Team '{self._team_manager.team_id}': Task notifier orchestrator is now monitoring TaskPlan events.")
|
|
54
54
|
|
|
55
|
-
async def _handle_tasks_changed(self, payload: Union[
|
|
55
|
+
async def _handle_tasks_changed(self, payload: Union[TasksCreatedEvent, TaskStatusUpdatedEvent], **kwargs):
|
|
56
56
|
"""
|
|
57
|
-
Orchestrates the agent activation workflow upon any change to the task
|
|
57
|
+
Orchestrates the agent activation workflow upon any change to the task plan.
|
|
58
58
|
"""
|
|
59
59
|
team_id = self._team_manager.team_id
|
|
60
|
-
logger.info(f"Team '{team_id}': Task
|
|
60
|
+
logger.info(f"Team '{team_id}': Task plan changed ({type(payload).__name__}). Orchestrating activation check.")
|
|
61
61
|
|
|
62
62
|
# If a new batch of tasks was added, it's a new "wave" of work.
|
|
63
63
|
# We must reset the policy's memory of who has been activated.
|
|
64
|
-
if isinstance(payload,
|
|
65
|
-
logger.info(f"Team '{team_id}': New tasks
|
|
64
|
+
if isinstance(payload, TasksCreatedEvent):
|
|
65
|
+
logger.info(f"Team '{team_id}': New tasks created. Resetting activation policy.")
|
|
66
66
|
self._policy.reset()
|
|
67
67
|
|
|
68
|
-
# 1. DATA FETCHING: Get the current state of runnable tasks from the
|
|
69
|
-
runnable_tasks = self.
|
|
68
|
+
# 1. DATA FETCHING: Get the current state of runnable tasks from the plan.
|
|
69
|
+
runnable_tasks = self._task_plan.get_next_runnable_tasks()
|
|
70
70
|
|
|
71
71
|
if not runnable_tasks:
|
|
72
72
|
logger.debug(f"Team '{team_id}': No runnable tasks found after change. No action needed.")
|
|
@@ -87,8 +87,8 @@ class SystemEventDrivenAgentTaskNotifier:
|
|
|
87
87
|
agent_runnable_tasks = [t for t in runnable_tasks if t.assignee_name == agent_name]
|
|
88
88
|
for task in agent_runnable_tasks:
|
|
89
89
|
# We only need to queue tasks that are NOT_STARTED.
|
|
90
|
-
if self.
|
|
91
|
-
self.
|
|
90
|
+
if self._task_plan.task_statuses.get(task.task_id) == TaskStatus.NOT_STARTED:
|
|
91
|
+
self._task_plan.update_task_status(
|
|
92
92
|
task_id=task.task_id,
|
|
93
93
|
status=TaskStatus.QUEUED,
|
|
94
94
|
agent_name="SystemTaskNotifier"
|
|
@@ -16,7 +16,7 @@ class TaskNotificationMode(str, Enum):
|
|
|
16
16
|
|
|
17
17
|
SYSTEM_EVENT_DRIVEN = "system_event_driven"
|
|
18
18
|
"""
|
|
19
|
-
In this mode, the agent team framework automatically monitors the
|
|
19
|
+
In this mode, the agent team framework automatically monitors the TaskPlan
|
|
20
20
|
and sends notifications to agents when their assigned tasks become runnable.
|
|
21
21
|
"""
|
|
22
22
|
|
|
@@ -141,8 +141,8 @@ class AgentTeamApp(App):
|
|
|
141
141
|
focused_data = self.focused_node_data
|
|
142
142
|
if focused_data and focused_data.get("type") in ['team', 'subteam']:
|
|
143
143
|
node_name = focused_data['name']
|
|
144
|
-
task_plan = self.store.
|
|
145
|
-
task_statuses = self.store.
|
|
144
|
+
task_plan = self.store.get_task_plan_tasks(node_name)
|
|
145
|
+
task_statuses = self.store.get_task_plan_statuses(node_name)
|
|
146
146
|
await focus_pane.update_content(
|
|
147
147
|
node_data=focused_data,
|
|
148
148
|
history=[], # No history for teams
|
|
@@ -169,8 +169,8 @@ class AgentTeamApp(App):
|
|
|
169
169
|
task_plan = None
|
|
170
170
|
task_statuses = None
|
|
171
171
|
if node_type in ['team', 'subteam']:
|
|
172
|
-
task_plan = self.store.
|
|
173
|
-
task_statuses = self.store.
|
|
172
|
+
task_plan = self.store.get_task_plan_tasks(node_name)
|
|
173
|
+
task_statuses = self.store.get_task_plan_statuses(node_name)
|
|
174
174
|
|
|
175
175
|
sidebar = self.query_one(AgentListSidebar)
|
|
176
176
|
focus_pane = self.query_one(FocusPane)
|
|
@@ -17,8 +17,8 @@ from autobyteus.agent.streaming.stream_event_payloads import (
|
|
|
17
17
|
from autobyteus.agent_team.streaming.agent_team_stream_events import AgentTeamStreamEvent
|
|
18
18
|
from autobyteus.agent_team.streaming.agent_team_stream_event_payloads import AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload, AgentTeamPhaseTransitionData
|
|
19
19
|
from autobyteus.task_management.task import Task
|
|
20
|
-
from autobyteus.task_management.events import
|
|
21
|
-
from autobyteus.task_management.
|
|
20
|
+
from autobyteus.task_management.events import TasksCreatedEvent, TaskStatusUpdatedEvent
|
|
21
|
+
from autobyteus.task_management.base_task_plan import TaskStatus
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ class TUIStateStore:
|
|
|
46
46
|
self._pending_approvals: Dict[str, ToolInvocationApprovalRequestedData] = {}
|
|
47
47
|
self._speaking_agents: Dict[str, bool] = {}
|
|
48
48
|
|
|
49
|
-
# State for task
|
|
49
|
+
# State for task plans
|
|
50
50
|
self._task_plans: Dict[str, List[Task]] = {} # team_name -> List[Task]
|
|
51
51
|
self._task_statuses: Dict[str, Dict[str, TaskStatus]] = {} # team_name -> {task_id: status}
|
|
52
52
|
|
|
@@ -85,15 +85,15 @@ class TUIStateStore:
|
|
|
85
85
|
self._team_event_history[parent_name] = []
|
|
86
86
|
self._team_event_history[parent_name].append(event)
|
|
87
87
|
|
|
88
|
-
if event.event_source_type == "
|
|
88
|
+
if event.event_source_type == "TASK_PLAN":
|
|
89
89
|
team_name_key = parent_name
|
|
90
|
-
if isinstance(event.data,
|
|
90
|
+
if isinstance(event.data, TasksCreatedEvent):
|
|
91
91
|
if team_name_key not in self._task_plans: self._task_plans[team_name_key] = []
|
|
92
92
|
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
93
93
|
self._task_plans[team_name_key].extend(event.data.tasks)
|
|
94
94
|
for task in event.data.tasks:
|
|
95
95
|
self._task_statuses[team_name_key][task.task_id] = TaskStatus.NOT_STARTED
|
|
96
|
-
logger.debug(f"TUI State:
|
|
96
|
+
logger.debug(f"TUI State: Created {len(event.data.tasks)} tasks in plan for '{team_name_key}'.")
|
|
97
97
|
|
|
98
98
|
elif isinstance(event.data, TaskStatusUpdatedEvent):
|
|
99
99
|
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
@@ -164,10 +164,10 @@ class TUIStateStore:
|
|
|
164
164
|
def get_pending_approval_for_agent(self, agent_name: str) -> Optional[ToolInvocationApprovalRequestedData]:
|
|
165
165
|
return self._pending_approvals.get(agent_name)
|
|
166
166
|
|
|
167
|
-
def
|
|
167
|
+
def get_task_plan_tasks(self, team_name: str) -> Optional[List[Task]]:
|
|
168
168
|
return self._task_plans.get(team_name)
|
|
169
169
|
|
|
170
|
-
def
|
|
170
|
+
def get_task_plan_statuses(self, team_name: str) -> Optional[Dict[str, TaskStatus]]:
|
|
171
171
|
return self._task_statuses.get(team_name)
|
|
172
172
|
|
|
173
173
|
def clear_pending_approval(self, agent_name: str):
|
|
@@ -14,7 +14,7 @@ from textual.containers import VerticalScroll, Horizontal
|
|
|
14
14
|
|
|
15
15
|
from autobyteus.agent.phases import AgentOperationalPhase
|
|
16
16
|
from autobyteus.agent_team.phases import AgentTeamOperationalPhase
|
|
17
|
-
from autobyteus.task_management.
|
|
17
|
+
from autobyteus.task_management.base_task_plan import TaskStatus
|
|
18
18
|
from autobyteus.task_management.task import Task
|
|
19
19
|
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
|
|
20
20
|
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
@@ -27,7 +27,7 @@ from .shared import (
|
|
|
27
27
|
USER_ICON, ASSISTANT_ICON, TEAM_ICON, AGENT_ICON, SYSTEM_TASK_ICON
|
|
28
28
|
)
|
|
29
29
|
from . import renderables
|
|
30
|
-
from .
|
|
30
|
+
from .task_plan_panel import TaskPlanPanel
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
@@ -211,7 +211,7 @@ class FocusPane(Static):
|
|
|
211
211
|
info_text.append(f"Status: {phase_icon} {phase.value}")
|
|
212
212
|
await log_container.mount(Static(Panel(info_text, title="Team Info", border_style="green", title_align="left")))
|
|
213
213
|
|
|
214
|
-
await log_container.mount(
|
|
214
|
+
await log_container.mount(TaskPlanPanel(tasks=task_plan, statuses=task_statuses, team_name=node_data['name']))
|
|
215
215
|
|
|
216
216
|
children_data = node_data.get("children", {})
|
|
217
217
|
if children_data:
|
|
@@ -4,7 +4,7 @@ Shared constants and data for TUI widgets.
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from autobyteus.agent.phases import AgentOperationalPhase
|
|
6
6
|
from autobyteus.agent_team.phases import AgentTeamOperationalPhase
|
|
7
|
-
from autobyteus.task_management.
|
|
7
|
+
from autobyteus.task_management.base_task_plan import TaskStatus
|
|
8
8
|
|
|
9
9
|
AGENT_PHASE_ICONS: Dict[AgentOperationalPhase, str] = {
|
|
10
10
|
AgentOperationalPhase.UNINITIALIZED: "⚪",
|
|
@@ -7,13 +7,13 @@ from rich.text import Text
|
|
|
7
7
|
from textual.widgets import Static
|
|
8
8
|
|
|
9
9
|
from autobyteus.task_management.task import Task
|
|
10
|
-
from autobyteus.task_management.
|
|
10
|
+
from autobyteus.task_management.base_task_plan import TaskStatus
|
|
11
11
|
from .shared import TASK_STATUS_ICONS, LOG_ICON
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
-
class
|
|
16
|
-
"""A widget to display the team's task
|
|
15
|
+
class TaskPlanPanel(Static):
|
|
16
|
+
"""A widget to display the team's task plan."""
|
|
17
17
|
|
|
18
18
|
def __init__(self, tasks: Optional[List[Task]], statuses: Dict[str, TaskStatus], team_name: str, **kwargs) -> None:
|
|
19
19
|
super().__init__(**kwargs)
|
|
@@ -23,7 +23,7 @@ class TaskBoardPanel(Static):
|
|
|
23
23
|
|
|
24
24
|
def compose(self) -> None:
|
|
25
25
|
if not self.tasks:
|
|
26
|
-
yield Static(Panel("No task plan has been published yet.", title="Task
|
|
26
|
+
yield Static(Panel("No task plan has been published yet.", title="Task Plan", border_style="yellow", title_align="left"))
|
|
27
27
|
return
|
|
28
28
|
|
|
29
29
|
table = Table(
|
|
@@ -79,4 +79,4 @@ class TaskBoardPanel(Static):
|
|
|
79
79
|
", ".join(dep_names)
|
|
80
80
|
)
|
|
81
81
|
|
|
82
|
-
yield Static(Panel(table, title="Task
|
|
82
|
+
yield Static(Panel(table, title="Task Plan", border_style="blue", title_align="left"))
|