autobyteus 1.1.3__py3-none-any.whl → 1.1.4__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 (148) hide show
  1. autobyteus/agent/agent.py +1 -1
  2. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
  3. autobyteus/agent/context/agent_config.py +36 -5
  4. autobyteus/agent/events/worker_event_dispatcher.py +1 -2
  5. autobyteus/agent/handlers/inter_agent_message_event_handler.py +1 -1
  6. autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
  7. autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
  8. autobyteus/agent/handlers/user_input_message_event_handler.py +1 -1
  9. autobyteus/agent/input_processor/__init__.py +1 -7
  10. autobyteus/agent/message/context_file_type.py +6 -0
  11. autobyteus/agent/message/send_message_to.py +68 -99
  12. autobyteus/agent/phases/discover.py +2 -1
  13. autobyteus/agent/runtime/agent_worker.py +1 -0
  14. autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
  15. autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
  16. autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
  17. autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
  18. autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
  19. autobyteus/agent/workspace/base_workspace.py +17 -2
  20. autobyteus/cli/__init__.py +1 -1
  21. autobyteus/cli/cli_display.py +1 -1
  22. autobyteus/cli/workflow_tui/__init__.py +4 -0
  23. autobyteus/cli/workflow_tui/app.py +210 -0
  24. autobyteus/cli/workflow_tui/state.py +189 -0
  25. autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
  26. autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
  27. autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
  28. autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
  29. autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
  30. autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
  31. autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
  32. autobyteus/events/event_types.py +3 -0
  33. autobyteus/llm/api/lmstudio_llm.py +37 -0
  34. autobyteus/llm/api/openai_compatible_llm.py +20 -3
  35. autobyteus/llm/llm_factory.py +2 -0
  36. autobyteus/llm/lmstudio_provider.py +89 -0
  37. autobyteus/llm/providers.py +1 -0
  38. autobyteus/llm/token_counter/token_counter_factory.py +2 -0
  39. autobyteus/tools/__init__.py +2 -0
  40. autobyteus/tools/ask_user_input.py +2 -1
  41. autobyteus/tools/bash/bash_executor.py +2 -1
  42. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
  43. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
  44. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
  45. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
  46. autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
  47. autobyteus/tools/browser/standalone/navigate_to.py +2 -0
  48. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
  49. autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
  50. autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
  51. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
  52. autobyteus/tools/file/file_reader.py +36 -9
  53. autobyteus/tools/file/file_writer.py +37 -9
  54. autobyteus/tools/functional_tool.py +5 -4
  55. autobyteus/tools/image_downloader.py +2 -0
  56. autobyteus/tools/mcp/tool_registrar.py +3 -1
  57. autobyteus/tools/pdf_downloader.py +2 -1
  58. autobyteus/tools/registry/tool_definition.py +12 -8
  59. autobyteus/tools/registry/tool_registry.py +50 -2
  60. autobyteus/tools/timer.py +2 -0
  61. autobyteus/tools/tool_category.py +14 -4
  62. autobyteus/tools/tool_meta.py +6 -1
  63. autobyteus/tools/tool_origin.py +10 -0
  64. autobyteus/workflow/agentic_workflow.py +93 -0
  65. autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
  66. autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
  67. autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
  68. autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
  69. autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
  70. autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
  71. autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
  72. autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
  73. autobyteus/workflow/context/__init__.py +17 -0
  74. autobyteus/workflow/context/team_manager.py +147 -0
  75. autobyteus/workflow/context/workflow_config.py +30 -0
  76. autobyteus/workflow/context/workflow_context.py +61 -0
  77. autobyteus/workflow/context/workflow_node_config.py +76 -0
  78. autobyteus/workflow/context/workflow_runtime_state.py +53 -0
  79. autobyteus/workflow/events/__init__.py +29 -0
  80. autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
  81. autobyteus/workflow/events/workflow_events.py +53 -0
  82. autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
  83. autobyteus/workflow/exceptions.py +8 -0
  84. autobyteus/workflow/factory/__init__.py +9 -0
  85. autobyteus/workflow/factory/workflow_factory.py +99 -0
  86. autobyteus/workflow/handlers/__init__.py +19 -0
  87. autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
  88. autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
  89. autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
  90. autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
  91. autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
  92. autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
  93. autobyteus/workflow/phases/__init__.py +11 -0
  94. autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
  95. autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
  96. autobyteus/workflow/runtime/__init__.py +13 -0
  97. autobyteus/workflow/runtime/workflow_runtime.py +82 -0
  98. autobyteus/workflow/runtime/workflow_worker.py +117 -0
  99. autobyteus/workflow/shutdown_steps/__init__.py +17 -0
  100. autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
  101. autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
  102. autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
  103. autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
  104. autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
  105. autobyteus/workflow/streaming/__init__.py +26 -0
  106. autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
  107. autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
  108. autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
  109. autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
  110. autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
  111. autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
  112. autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
  113. autobyteus/workflow/utils/__init__.py +9 -0
  114. autobyteus/workflow/utils/wait_for_idle.py +46 -0
  115. autobyteus/workflow/workflow_builder.py +151 -0
  116. {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -14
  117. {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/RECORD +134 -65
  118. {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/top_level.txt +1 -0
  119. examples/__init__.py +1 -0
  120. examples/discover_phase_transitions.py +104 -0
  121. examples/run_browser_agent.py +260 -0
  122. examples/run_google_slides_agent.py +286 -0
  123. examples/run_mcp_browser_client.py +174 -0
  124. examples/run_mcp_google_slides_client.py +270 -0
  125. examples/run_mcp_list_tools.py +189 -0
  126. examples/run_poem_writer.py +274 -0
  127. examples/run_sqlite_agent.py +293 -0
  128. examples/workflow/__init__.py +1 -0
  129. examples/workflow/run_basic_research_workflow.py +189 -0
  130. examples/workflow/run_code_review_workflow.py +269 -0
  131. examples/workflow/run_debate_workflow.py +212 -0
  132. examples/workflow/run_workflow_with_tui.py +153 -0
  133. autobyteus/agent/context/agent_phase_manager.py +0 -264
  134. autobyteus/agent/context/phases.py +0 -49
  135. autobyteus/agent/group/__init__.py +0 -0
  136. autobyteus/agent/group/agent_group.py +0 -164
  137. autobyteus/agent/group/agent_group_context.py +0 -81
  138. autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
  139. autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
  140. autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
  141. autobyteus/agent/workflow/__init__.py +0 -11
  142. autobyteus/agent/workflow/agentic_workflow.py +0 -89
  143. autobyteus/tools/mcp/registrar.py +0 -202
  144. autobyteus/workflow/simple_task.py +0 -98
  145. autobyteus/workflow/task.py +0 -147
  146. autobyteus/workflow/workflow.py +0 -49
  147. {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
  148. {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,36 @@
1
+ # file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_definition.py
2
+ import logging
3
+ from typing import Type, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .base_processor import BaseToolExecutionResultProcessor
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ToolExecutionResultProcessorDefinition:
11
+ """
12
+ Represents the definition of a tool execution result processor.
13
+ Contains its registered name and the class itself.
14
+ """
15
+ def __init__(self, name: str, processor_class: Type['BaseToolExecutionResultProcessor']):
16
+ """
17
+ Initializes the ToolExecutionResultProcessorDefinition.
18
+
19
+ Args:
20
+ name: The unique registered name of the processor.
21
+ processor_class: The class of the tool execution result processor.
22
+
23
+ Raises:
24
+ ValueError: If name is empty or processor_class is not a type.
25
+ """
26
+ if not name or not isinstance(name, str):
27
+ raise ValueError("Tool Execution Result Processor name must be a non-empty string.")
28
+ if not isinstance(processor_class, type):
29
+ raise ValueError("processor_class must be a class type.")
30
+
31
+ self.name: str = name
32
+ self.processor_class: Type['BaseToolExecutionResultProcessor'] = processor_class
33
+ logger.debug(f"ToolExecutionResultProcessorDefinition created: name='{name}', class='{processor_class.__name__}'.")
34
+
35
+ def __repr__(self) -> str:
36
+ return f"<ToolExecutionResultProcessorDefinition name='{self.name}', class='{self.processor_class.__name__}'>"
@@ -0,0 +1,36 @@
1
+ # file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_meta.py
2
+ import logging
3
+ from abc import ABCMeta
4
+
5
+ from .processor_registry import default_tool_execution_result_processor_registry
6
+ from .processor_definition import ToolExecutionResultProcessorDefinition
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class ToolExecutionResultProcessorMeta(ABCMeta):
11
+ """
12
+ Metaclass for BaseToolExecutionResultProcessor that automatically registers
13
+ concrete processor subclasses with the default registry.
14
+ """
15
+ def __init__(cls, name, bases, dct):
16
+ super().__init__(name, bases, dct)
17
+
18
+ if name == 'BaseToolExecutionResultProcessor' or getattr(cls, "__abstractmethods__", None):
19
+ logger.debug(f"Skipping registration for abstract tool execution result processor class: {name}")
20
+ return
21
+
22
+ try:
23
+ processor_name = cls.get_name()
24
+
25
+ if not processor_name or not isinstance(processor_name, str):
26
+ logger.error(f"Tool execution result processor class {name} must return a valid string from get_name(). Skipping.")
27
+ return
28
+
29
+ definition = ToolExecutionResultProcessorDefinition(name=processor_name, processor_class=cls)
30
+ default_tool_execution_result_processor_registry.register_processor(definition)
31
+ logger.info(f"Auto-registered tool execution result processor: '{processor_name}' from class {name}.")
32
+
33
+ except AttributeError as e:
34
+ logger.error(f"Tool execution result processor class {name} is missing 'get_name' method ({e}). Skipping registration.")
35
+ except Exception as e:
36
+ logger.error(f"Failed to auto-register tool execution result processor class {name}: {e}", exc_info=True)
@@ -0,0 +1,70 @@
1
+ # file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_registry.py
2
+ import logging
3
+ from typing import TYPE_CHECKING, Dict, List, Optional
4
+
5
+ from autobyteus.utils.singleton import SingletonMeta
6
+ from .processor_definition import ToolExecutionResultProcessorDefinition
7
+
8
+ if TYPE_CHECKING:
9
+ from .base_processor import BaseToolExecutionResultProcessor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class ToolExecutionResultProcessorRegistry(metaclass=SingletonMeta):
14
+ """
15
+ A singleton registry for ToolExecutionResultProcessorDefinition objects.
16
+ Processors are typically auto-registered via ToolExecutionResultProcessorMeta.
17
+ """
18
+
19
+ def __init__(self):
20
+ """Initializes the registry with an empty store."""
21
+ self._definitions: Dict[str, ToolExecutionResultProcessorDefinition] = {}
22
+ logger.info("ToolExecutionResultProcessorRegistry initialized.")
23
+
24
+ def register_processor(self, definition: ToolExecutionResultProcessorDefinition) -> None:
25
+ """
26
+ Registers a tool execution result processor definition.
27
+ """
28
+ if not isinstance(definition, ToolExecutionResultProcessorDefinition):
29
+ raise TypeError(f"Expected ToolExecutionResultProcessorDefinition instance, got {type(definition).__name__}.")
30
+
31
+ processor_name = definition.name
32
+ if processor_name in self._definitions:
33
+ logger.warning(f"Overwriting existing tool execution result processor definition for name: '{processor_name}'.")
34
+
35
+ self._definitions[processor_name] = definition
36
+ logger.info(f"Tool execution result processor definition '{processor_name}' registered successfully.")
37
+
38
+ def get_processor_definition(self, name: str) -> Optional[ToolExecutionResultProcessorDefinition]:
39
+ """
40
+ Retrieves a processor definition by its name.
41
+ """
42
+ return self._definitions.get(name)
43
+
44
+ def get_processor(self, name: str) -> Optional['BaseToolExecutionResultProcessor']:
45
+ """
46
+ Retrieves an instance of a processor by its name.
47
+ """
48
+ definition = self.get_processor_definition(name)
49
+ if definition:
50
+ try:
51
+ return definition.processor_class()
52
+ except Exception as e:
53
+ logger.error(f"Failed to instantiate tool execution result processor '{name}': {e}", exc_info=True)
54
+ return None
55
+ return None
56
+
57
+ def list_processor_names(self) -> List[str]:
58
+ """
59
+ Returns a list of names of all registered processor definitions.
60
+ """
61
+ return list(self._definitions.keys())
62
+
63
+ def get_all_definitions(self) -> Dict[str, ToolExecutionResultProcessorDefinition]:
64
+ """
65
+ Returns a dictionary of all registered processor definitions.
66
+ """
67
+ return dict(self._definitions)
68
+
69
+ # Default instance of the registry
70
+ default_tool_execution_result_processor_registry = ToolExecutionResultProcessorRegistry()
@@ -1,5 +1,6 @@
1
1
  # file: autobyteus/autobyteus/agent/workspace/base_workspace.py
2
2
  import logging
3
+ import uuid
3
4
  from abc import ABC, abstractmethod
4
5
  from typing import Optional, Any, Dict, TYPE_CHECKING
5
6
  from autobyteus.tools.parameter_schema import ParameterSchema
@@ -29,7 +30,8 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
29
30
  """
30
31
  self._config: WorkspaceConfig = config or WorkspaceConfig()
31
32
  self.context: Optional['AgentContext'] = None
32
- logger.debug(f"{self.__class__.__name__} instance initialized. Context pending injection.")
33
+ self.workspace_id: str = str(uuid.uuid4())
34
+ logger.debug(f"{self.__class__.__name__} instance initialized with ID {self.workspace_id}. Context pending injection.")
33
35
 
34
36
  def set_context(self, context: 'AgentContext'):
35
37
  """
@@ -53,6 +55,19 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
53
55
  """Configuration for the workspace. Implementations can use this as needed."""
54
56
  return self._config
55
57
 
58
+ @abstractmethod
59
+ def get_base_path(self) -> str:
60
+ """Returns the base path for the workspace, which can be used to resolve relative paths."""
61
+ pass
62
+
63
+ def get_name(self) -> str:
64
+ """
65
+ Returns a user-friendly name for this workspace instance.
66
+ By default, it returns the unique workspace ID.
67
+ Subclasses can override this to provide a more descriptive name (e.g., a directory name).
68
+ """
69
+ return self.workspace_id
70
+
56
71
  # --- Methods for self-description ---
57
72
 
58
73
  @classmethod
@@ -74,4 +89,4 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
74
89
  pass
75
90
 
76
91
  def __repr__(self) -> str:
77
- return f"<{self.__class__.__name__} agent_id='{self.agent_id or 'N/A'}>"
92
+ return f"<{self.__class__.__name__} workspace_id='{self.workspace_id}' agent_id='{self.agent_id or 'N/A'}>"
@@ -6,6 +6,6 @@ from .agent_cli import run
6
6
  from .cli_display import InteractiveCLIDisplay
7
7
 
8
8
  __all__ = [
9
- "run",
9
+ "run",
10
10
  "InteractiveCLIDisplay",
11
11
  ]
@@ -4,7 +4,7 @@ import sys
4
4
  from typing import Optional, List, Dict, Any
5
5
  import json
6
6
 
7
- from autobyteus.agent.context.phases import AgentOperationalPhase
7
+ from autobyteus.agent.phases.phase_enum import AgentOperationalPhase
8
8
  from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
9
9
  from autobyteus.agent.streaming.stream_event_payloads import (
10
10
  AssistantChunkData,
@@ -0,0 +1,4 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/__init__.py
2
+ """
3
+ A Textual-based TUI for interacting with Agentic Workflows.
4
+ """
@@ -0,0 +1,210 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/app.py
2
+ """
3
+ The main Textual application class for the workflow 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.workflow.agentic_workflow import AgenticWorkflow
16
+ from autobyteus.workflow.streaming.workflow_event_stream import WorkflowEventStream
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.workflow.streaming.workflow_stream_event_payloads import AgentEventRebroadcastPayload, WorkflowPhaseTransitionData
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 WorkflowApp(App):
30
+ """A Textual TUI for interacting with an agentic workflow, 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, workflow: AgenticWorkflow, **kwargs):
44
+ super().__init__(**kwargs)
45
+ self.workflow = workflow
46
+ self.store = TUIStateStore(workflow=self.workflow)
47
+ self.workflow_stream: Optional[WorkflowEventStream] = 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.workflow.start()
61
+ self.workflow_stream = WorkflowEventStream(self.workflow)
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.workflow.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_workflow_events(), name="workflow_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("Workflow TUI mounted, workflow listener and throttled UI updater started.")
76
+
77
+ async def on_unmount(self) -> None:
78
+ if self.workflow and self.workflow.is_running:
79
+ await self.workflow.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_workflow_events(self) -> None:
96
+ """A background worker that forwards workflow events to the state store and updates the UI."""
97
+ if not self.workflow_stream: return
98
+ try:
99
+ async for event in self.workflow_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
+ # This is for components like the FocusPane's text stream, which needs
108
+ # to be as low-latency as possible. The actual UI update is buffered.
109
+ if isinstance(event.data, AgentEventRebroadcastPayload):
110
+ payload = event.data
111
+ agent_name = payload.agent_name
112
+ agent_event = payload.agent_event
113
+ focus_pane = self.query_one(FocusPane)
114
+
115
+ is_currently_focused = (focus_pane._focused_node_data and focus_pane._focused_node_data.get('name') == agent_name)
116
+
117
+ # If the event is for the currently focused agent, send the event
118
+ # to be buffered and eventually rendered.
119
+ if is_currently_focused:
120
+ await focus_pane.add_agent_event(agent_event)
121
+
122
+ except asyncio.CancelledError:
123
+ logger.info("Workflow event listener task was cancelled.")
124
+ except Exception:
125
+ logger.error("Critical error in workflow TUI event listener", exc_info=True)
126
+ finally:
127
+ if self.workflow_stream: await self.workflow_stream.close()
128
+
129
+ # --- Reactive Watchers ---
130
+
131
+ async def watch_store_version(self, new_version: int):
132
+ """
133
+ Reacts to changes in the store version. This is now called by the throttled
134
+ updater, not on every event. Its main job is to update less-frequently
135
+ changing components like the sidebar tree and workflow dashboards.
136
+ """
137
+ sidebar = self.query_one(AgentListSidebar)
138
+ focus_pane = self.query_one(FocusPane)
139
+
140
+ # Fetch fresh data from the store for the update
141
+ tree_data = self.store.get_tree_data()
142
+ agent_phases = self.store._agent_phases
143
+ workflow_phases = self.store._workflow_phases
144
+ speaking_agents = self.store._speaking_agents
145
+
146
+ # Update sidebar
147
+ sidebar.update_tree(tree_data, agent_phases, workflow_phases, speaking_agents)
148
+
149
+ # Intelligently update the focus pane
150
+ focused_data = self.focused_node_data
151
+ if focused_data and focused_data.get("type") in ['workflow', 'subworkflow']:
152
+ # If a workflow/subworkflow is focused, its dashboard might be out of date.
153
+ # A full re-render is cheap and ensures consistency for its title and panels.
154
+ history = self.store.get_history_for_node(focused_data['name'], focused_data['type'])
155
+ await focus_pane.update_content(
156
+ node_data=focused_data,
157
+ history=history,
158
+ pending_approval=None,
159
+ all_agent_phases=agent_phases,
160
+ all_workflow_phases=workflow_phases
161
+ )
162
+ elif focused_data and focused_data.get("type") == 'agent':
163
+ # For agents, we only need to update the title status, not the whole log.
164
+ focus_pane.update_current_node_status(agent_phases, workflow_phases)
165
+
166
+
167
+ async def watch_focused_node_data(self, new_node_data: Optional[Dict[str, Any]]):
168
+ """Reacts to changes in which node is focused. Primarily used for full pane reloads on user click."""
169
+ if not new_node_data: return
170
+
171
+ node_name = new_node_data['name']
172
+ node_type = new_node_data['type']
173
+
174
+ history = self.store.get_history_for_node(node_name, node_type)
175
+ pending_approval = self.store.get_pending_approval_for_agent(node_name) if node_type == 'agent' else None
176
+
177
+ sidebar = self.query_one(AgentListSidebar)
178
+ focus_pane = self.query_one(FocusPane)
179
+
180
+ await focus_pane.update_content(
181
+ node_data=new_node_data,
182
+ history=history,
183
+ pending_approval=pending_approval,
184
+ all_agent_phases=self.store._agent_phases,
185
+ all_workflow_phases=self.store._workflow_phases
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.workflow.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.workflow.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,189 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/state.py
2
+ """
3
+ Defines a centralized state store for the TUI application, following state management best practices.
4
+ """
5
+ import logging
6
+ from typing import Dict, List, Optional, Any
7
+ import copy
8
+
9
+ from autobyteus.agent.context import AgentConfig
10
+ from autobyteus.workflow.agentic_workflow import AgenticWorkflow
11
+ from autobyteus.agent.phases import AgentOperationalPhase
12
+ from autobyteus.workflow.phases import WorkflowOperationalPhase
13
+ from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
14
+ from autobyteus.agent.streaming.stream_event_payloads import (
15
+ AgentOperationalPhaseTransitionData, ToolInvocationApprovalRequestedData,
16
+ AssistantChunkData, AssistantCompleteResponseData
17
+ )
18
+ from autobyteus.workflow.streaming.workflow_stream_events import WorkflowStreamEvent
19
+ from autobyteus.workflow.streaming.workflow_stream_event_payloads import AgentEventRebroadcastPayload, SubWorkflowEventRebroadcastPayload, WorkflowPhaseTransitionData
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class TUIStateStore:
24
+ """
25
+ A centralized store for all TUI-related state.
26
+
27
+ This class acts as the single source of truth for the UI. It processes events
28
+ from the backend and updates its state. The main App class can then react to
29
+ these state changes to update the UI components declaratively. This is a plain
30
+ Python class and does not use Textual reactive properties.
31
+ """
32
+
33
+ def __init__(self, workflow: AgenticWorkflow):
34
+ self.workflow_name = workflow.name
35
+ self.workflow_role = workflow.role
36
+
37
+ self.focused_node_data: Optional[Dict[str, Any]] = None
38
+
39
+ self._node_roles: Dict[str, str] = self._extract_node_roles(workflow)
40
+ self._nodes: Dict[str, Any] = self._initialize_root_node()
41
+ self._agent_phases: Dict[str, AgentOperationalPhase] = {}
42
+ self._workflow_phases: Dict[str, WorkflowOperationalPhase] = {self.workflow_name: WorkflowOperationalPhase.UNINITIALIZED}
43
+ self._agent_event_history: Dict[str, List[AgentStreamEvent]] = {}
44
+ self._workflow_event_history: Dict[str, List[WorkflowStreamEvent]] = {self.workflow_name: []}
45
+ self._pending_approvals: Dict[str, ToolInvocationApprovalRequestedData] = {}
46
+ self._speaking_agents: Dict[str, bool] = {}
47
+
48
+ # REMOVED: The complex stream aggregator is the source of the bug.
49
+ # self._agent_stream_aggregators: Dict[str, Dict[str, str]] = {}
50
+
51
+ # Version counter to signal state changes to the UI
52
+ self.version = 0
53
+
54
+ def _extract_node_roles(self, workflow: AgenticWorkflow) -> Dict[str, str]:
55
+ """Builds a map of node names to their defined roles from the config."""
56
+ roles = {}
57
+ if workflow._runtime and workflow._runtime.context and workflow._runtime.context.config:
58
+ for node_config in workflow._runtime.context.config.nodes:
59
+ role = getattr(node_config.node_definition, 'role', None)
60
+ if role:
61
+ roles[node_config.name] = role
62
+ return roles
63
+
64
+ def _initialize_root_node(self) -> Dict[str, Any]:
65
+ """Creates the initial root node for the state tree."""
66
+ return {
67
+ self.workflow_name: {
68
+ "type": "workflow",
69
+ "name": self.workflow_name,
70
+ "role": self.workflow_role,
71
+ "children": {}
72
+ }
73
+ }
74
+
75
+ def process_event(self, event: WorkflowStreamEvent):
76
+ """
77
+ The main entry point for processing events from the backend.
78
+ This method acts as a reducer, updating the state based on the event.
79
+ """
80
+ if event.event_source_type == "WORKFLOW" and isinstance(event.data, WorkflowPhaseTransitionData):
81
+ self._workflow_phases[self.workflow_name] = event.data.new_phase
82
+
83
+ self._process_event_recursively(event, self.workflow_name)
84
+
85
+ # Increment version to signal that the state has changed.
86
+ self.version += 1
87
+
88
+ # REMOVED: The flush aggregator logic is no longer needed.
89
+ # def _flush_aggregator_for_agent(self, agent_name: str): ...
90
+
91
+ def _process_event_recursively(self, event: WorkflowStreamEvent, parent_name: str):
92
+ """Recursively processes events to build up the state tree."""
93
+ if parent_name not in self._workflow_event_history:
94
+ self._workflow_event_history[parent_name] = []
95
+ self._workflow_event_history[parent_name].append(event)
96
+
97
+ # AGENT EVENT (LEAF NODE)
98
+ if isinstance(event.data, AgentEventRebroadcastPayload):
99
+ payload = event.data
100
+ agent_name = payload.agent_name
101
+ agent_event = payload.agent_event
102
+
103
+ if agent_name not in self._agent_event_history:
104
+ self._agent_event_history[agent_name] = []
105
+ if self._find_node(parent_name):
106
+ agent_role = self._node_roles.get(agent_name, "Agent")
107
+ self._add_node(agent_name, {"type": "agent", "name": agent_name, "role": agent_role, "children": {}}, parent_name)
108
+ else:
109
+ logger.error(f"Cannot add agent node '{agent_name}': parent '{parent_name}' not found in state tree.")
110
+
111
+ # SIMPLIFIED LOGIC: Always append the event to the history, regardless of focus.
112
+ # This ensures the history is always a complete and accurate log of what happened.
113
+ self._agent_event_history[agent_name].append(agent_event)
114
+
115
+ # --- State update logic for specific events (applies to both focused and non-focused) ---
116
+ if agent_event.event_type == AgentStreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION:
117
+ phase_data: AgentOperationalPhaseTransitionData = agent_event.data
118
+ self._agent_phases[agent_name] = phase_data.new_phase
119
+ if agent_name in self._pending_approvals:
120
+ del self._pending_approvals[agent_name]
121
+ elif agent_event.event_type == AgentStreamEventType.AGENT_IDLE:
122
+ self._agent_phases[agent_name] = AgentOperationalPhase.IDLE
123
+ elif agent_event.event_type == AgentStreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED:
124
+ self._pending_approvals[agent_name] = agent_event.data
125
+
126
+ # SUB-WORKFLOW EVENT (BRANCH NODE)
127
+ elif isinstance(event.data, SubWorkflowEventRebroadcastPayload):
128
+ payload = event.data
129
+ sub_workflow_name = payload.sub_workflow_node_name
130
+ sub_workflow_event = payload.sub_workflow_event
131
+
132
+ sub_workflow_node = self._find_node(sub_workflow_name)
133
+ if not sub_workflow_node:
134
+ role = self._node_roles.get(sub_workflow_name, "Sub-Workflow")
135
+ self._add_node(sub_workflow_name, {"type": "subworkflow", "name": sub_workflow_name, "role": role, "children": {}}, parent_name)
136
+
137
+ if sub_workflow_event.event_source_type == "WORKFLOW" and isinstance(sub_workflow_event.data, WorkflowPhaseTransitionData):
138
+ self._workflow_phases[sub_workflow_name] = sub_workflow_event.data.new_phase
139
+
140
+ self._process_event_recursively(sub_workflow_event, parent_name=sub_workflow_name)
141
+
142
+ def _add_node(self, node_name: str, node_data: Dict, parent_name: str):
143
+ """Adds a node to the state tree under a specific parent."""
144
+ parent = self._find_node(parent_name)
145
+ if parent:
146
+ parent["children"][node_name] = node_data
147
+ else:
148
+ logger.error(f"Could not find parent node '{parent_name}' to add child '{node_name}'.")
149
+
150
+ def _find_node(self, node_name: str, tree: Optional[Dict] = None) -> Optional[Dict]:
151
+ """Recursively finds a node by name in the state tree."""
152
+ if tree is None:
153
+ tree = self._nodes
154
+
155
+ for name, node_data in tree.items():
156
+ if name == node_name:
157
+ return node_data
158
+ if node_data.get("children"):
159
+ found = self._find_node(node_name, node_data.get("children"))
160
+ if found:
161
+ return found
162
+ return None
163
+
164
+ def get_tree_data(self) -> Dict:
165
+ """Constructs a serializable representation of the tree for the sidebar."""
166
+ return copy.deepcopy(self._nodes)
167
+
168
+ def get_history_for_node(self, node_name: str, node_type: str) -> List:
169
+ """Retrieves the event history for a given node."""
170
+ if node_type == 'agent':
171
+ # REMOVED: Flushing is no longer necessary as the history is always complete.
172
+ return self._agent_event_history.get(node_name, [])
173
+ elif node_type in ['workflow', 'subworkflow']:
174
+ return []
175
+ return []
176
+
177
+ def get_pending_approval_for_agent(self, agent_name: str) -> Optional[ToolInvocationApprovalRequestedData]:
178
+ """Gets pending approval data for a specific agent."""
179
+ return self._pending_approvals.get(agent_name)
180
+
181
+ def clear_pending_approval(self, agent_name: str):
182
+ """Clears a pending approval after it's been handled."""
183
+ if agent_name in self._pending_approvals:
184
+ del self._pending_approvals[agent_name]
185
+
186
+ def set_focused_node(self, node_data: Optional[Dict[str, Any]]):
187
+ """Sets the currently focused node in the state."""
188
+ # REMOVED: Flushing logic is no longer needed here.
189
+ self.focused_node_data = node_data
@@ -0,0 +1,6 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/__init__.py
2
+ """
3
+ Custom Textual widgets for the workflow TUI.
4
+ """
5
+ from . import renderables
6
+ from .logo import Logo