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,149 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py
2
+ """
3
+ Defines the sidebar widget that lists all nodes in the workflow hierarchy.
4
+ """
5
+ import logging
6
+ from typing import Dict, Any, Optional
7
+
8
+ from textual.message import Message
9
+ from textual.widgets import Static, Tree
10
+ from textual.widgets.tree import TreeNode
11
+ from textual.containers import Vertical
12
+
13
+ from autobyteus.agent.phases import AgentOperationalPhase
14
+ from autobyteus.workflow.phases import WorkflowOperationalPhase
15
+ from .shared import (
16
+ AGENT_PHASE_ICONS, WORKFLOW_PHASE_ICONS, SUB_WORKFLOW_ICON,
17
+ WORKFLOW_ICON, SPEAKING_ICON, DEFAULT_ICON
18
+ )
19
+ from .logo import Logo
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class AgentListSidebar(Static):
24
+ """A widget to display the hierarchical list of workflow nodes. This is a dumb
25
+ rendering component driven by the TUIStateStore."""
26
+
27
+ class NodeSelected(Message):
28
+ """Posted when any node is selected in the tree."""
29
+ def __init__(self, node_data: Dict[str, Any]) -> None:
30
+ self.node_data = node_data
31
+ super().__init__()
32
+
33
+ def __init__(self, *args, **kwargs) -> None:
34
+ super().__init__(*args, **kwargs)
35
+ self._node_map: Dict[str, TreeNode] = {} # Maps node names to TreeNode objects
36
+
37
+ def compose(self):
38
+ with Vertical():
39
+ yield Tree("Workflow", id="agent-tree")
40
+ yield Logo()
41
+
42
+ def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
43
+ """Handle node selection from the tree."""
44
+ if event.node.data:
45
+ self.post_message(self.NodeSelected(event.node.data))
46
+ event.stop()
47
+
48
+ def _build_label(self, name: str, node_data: Dict, agent_phases: Dict, workflow_phases: Dict, speaking_agents: Dict) -> str:
49
+ """Constructs the display label for a tree node."""
50
+ node_type = node_data["type"]
51
+ icon = DEFAULT_ICON
52
+
53
+ if node_type == "agent":
54
+ phase = agent_phases.get(name, AgentOperationalPhase.UNINITIALIZED)
55
+ icon = SPEAKING_ICON if speaking_agents.get(name) else AGENT_PHASE_ICONS.get(phase, DEFAULT_ICON)
56
+ label = f"{icon} {name}"
57
+ elif node_type in ["workflow", "subworkflow"]:
58
+ phase = workflow_phases.get(name, WorkflowOperationalPhase.UNINITIALIZED)
59
+ default_icon = WORKFLOW_ICON if node_type == "workflow" else SUB_WORKFLOW_ICON
60
+ icon = WORKFLOW_PHASE_ICONS.get(phase, default_icon)
61
+ role = node_data.get("role")
62
+ label = f"{icon} {role or name}"
63
+ if role and role != name:
64
+ label += f" ({name})"
65
+ else:
66
+ label = f"{icon} {name}"
67
+
68
+ return label
69
+
70
+ def update_tree(self, tree_data: Dict, agent_phases: Dict[str, AgentOperationalPhase], workflow_phases: Dict[str, WorkflowOperationalPhase], speaking_agents: Dict[str, bool]):
71
+ """
72
+ Performs an in-place update of the tree to reflect the new state,
73
+ avoiding a full rebuild for better performance and preserving UI state like expansion.
74
+ """
75
+ tree = self.query_one(Tree)
76
+
77
+ if not tree_data:
78
+ tree.root.set_label("Initializing workflow...")
79
+ return
80
+
81
+ root_name = list(tree_data.keys())[0]
82
+ root_node_data = tree_data[root_name]
83
+
84
+ # Kick off the recursive update from the root.
85
+ self._update_node_recursively(tree.root, root_node_data, agent_phases, workflow_phases, speaking_agents)
86
+
87
+ # Ensure the root is expanded on the first run.
88
+ if not tree.root.is_expanded:
89
+ tree.root.expand()
90
+
91
+ def _update_node_recursively(self, ui_node: TreeNode, node_data: Dict, agent_phases: Dict, workflow_phases: Dict, speaking_agents: Dict):
92
+ """Recursively updates a node and reconciles its children."""
93
+ # 1. Update the current node's label and data
94
+ name = node_data['name']
95
+ label = self._build_label(name, node_data, agent_phases, workflow_phases, speaking_agents)
96
+ ui_node.set_label(label)
97
+ ui_node.data = node_data
98
+ self._node_map[name] = ui_node # Ensure map is always up-to-date
99
+
100
+ # 2. Reconcile children
101
+ new_children_data = node_data.get("children", {})
102
+ existing_ui_children_by_name = {child.data['name']: child for child in ui_node.children if child.data}
103
+
104
+ # Add new nodes and update existing ones
105
+ for child_name, child_data in new_children_data.items():
106
+ if child_name in existing_ui_children_by_name:
107
+ # Node exists, so we recursively update it
108
+ child_ui_node = existing_ui_children_by_name[child_name]
109
+ self._update_node_recursively(child_ui_node, child_data, agent_phases, workflow_phases, speaking_agents)
110
+ else:
111
+ # Node is new, so we add it
112
+ new_child_label = self._build_label(child_name, child_data, agent_phases, workflow_phases, speaking_agents)
113
+ is_leaf = child_data.get("children", {}) == {} and child_data['type'] == 'agent'
114
+
115
+ if is_leaf:
116
+ new_ui_node = ui_node.add_leaf(new_child_label, data=child_data)
117
+ else:
118
+ new_ui_node = ui_node.add(new_child_label, data=child_data)
119
+ # Since this is a new branch, we must build its children too
120
+ self._update_node_recursively(new_ui_node, child_data, agent_phases, workflow_phases, speaking_agents)
121
+
122
+ self._node_map[child_name] = new_ui_node
123
+
124
+ # Remove old nodes that no longer exist in the new data
125
+ nodes_to_remove = []
126
+ for existing_child_name, existing_child_node in existing_ui_children_by_name.items():
127
+ if existing_child_name not in new_children_data:
128
+ nodes_to_remove.append(existing_child_node)
129
+ if existing_child_name in self._node_map:
130
+ del self._node_map[existing_child_name]
131
+
132
+ for node in nodes_to_remove:
133
+ node.remove()
134
+
135
+ def update_selection(self, node_name: Optional[str]):
136
+ """Updates the tree's selection and expands parents to make it visible."""
137
+ if not node_name or node_name not in self._node_map:
138
+ return
139
+
140
+ tree = self.query_one(Tree)
141
+ node_to_select = self._node_map[node_name]
142
+
143
+ parent = node_to_select.parent
144
+ while parent:
145
+ parent.expand()
146
+ parent = parent.parent
147
+
148
+ tree.select_node(node_to_select)
149
+ tree.scroll_to_node(node_to_select)
@@ -0,0 +1,335 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/focus_pane.py
2
+ """
3
+ Defines the main focus pane widget for displaying detailed logs or summaries.
4
+ """
5
+ import logging
6
+ import json
7
+ from typing import Optional, List, Any, Dict
8
+
9
+ from rich.text import Text
10
+ from rich.panel import Panel
11
+ from rich.syntax import Syntax
12
+ from textual.message import Message
13
+ from textual.widgets import Input, Static, Button
14
+ from textual.containers import VerticalScroll, Horizontal
15
+
16
+ from autobyteus.agent.phases import AgentOperationalPhase
17
+ from autobyteus.workflow.phases import WorkflowOperationalPhase
18
+ from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
19
+ from autobyteus.agent.streaming.stream_event_payloads import (
20
+ AgentOperationalPhaseTransitionData, AssistantChunkData, AssistantCompleteResponseData,
21
+ ErrorEventData, ToolInteractionLogEntryData, ToolInvocationApprovalRequestedData, ToolInvocationAutoExecutingData
22
+ )
23
+ from .shared import (
24
+ AGENT_PHASE_ICONS, WORKFLOW_PHASE_ICONS, SUB_WORKFLOW_ICON, DEFAULT_ICON,
25
+ USER_ICON, ASSISTANT_ICON, WORKFLOW_ICON, AGENT_ICON
26
+ )
27
+ from . import renderables
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ class FocusPane(Static):
32
+ """
33
+ A widget to display detailed logs for agents or high-level dashboards for workflows.
34
+ This is a dumb rendering component driven by the TUIStateStore.
35
+ """
36
+
37
+ class MessageSubmitted(Message):
38
+ def __init__(self, text: str, agent_name: str) -> None:
39
+ self.text = text
40
+ self.agent_name = agent_name
41
+ super().__init__()
42
+
43
+ class ApprovalSubmitted(Message):
44
+ def __init__(self, agent_name: str, invocation_id: str, is_approved: bool, reason: Optional[str]) -> None:
45
+ self.agent_name = agent_name
46
+ self.invocation_id = invocation_id
47
+ self.is_approved = is_approved
48
+ self.reason = reason
49
+ super().__init__()
50
+
51
+ def __init__(self, *args, **kwargs) -> None:
52
+ super().__init__(*args, **kwargs)
53
+ self._focused_node_data: Optional[Dict[str, Any]] = None
54
+ self._pending_approval_data: Optional[ToolInvocationApprovalRequestedData] = None
55
+
56
+ # State variables for streaming
57
+ self._thinking_widget: Optional[Static] = None
58
+ self._thinking_text: Optional[Text] = None
59
+ self._assistant_content_widget: Optional[Static] = None
60
+ self._assistant_content_text: Optional[Text] = None
61
+
62
+ # Buffers for batched UI updates to improve performance
63
+ self._reasoning_buffer: str = ""
64
+ self._content_buffer: str = ""
65
+
66
+ def compose(self):
67
+ yield Static("Select a node from the sidebar", id="focus-pane-title")
68
+ yield VerticalScroll(id="focus-pane-log-container")
69
+ yield Horizontal(id="approval-buttons")
70
+ yield Input(placeholder="Select an agent to send messages...", id="focus-pane-input", disabled=True)
71
+
72
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
73
+ if event.value and self._focused_node_data and self._focused_node_data.get("type") == 'agent':
74
+ log_container = self.query_one("#focus-pane-log-container")
75
+ user_message_text = Text(f"{USER_ICON} You: {event.value}", style="bright_blue")
76
+ await log_container.mount(Static(""))
77
+ await log_container.mount(Static(user_message_text))
78
+ log_container.scroll_end(animate=False)
79
+
80
+ self.post_message(self.MessageSubmitted(event.value, self._focused_node_data['name']))
81
+ self.query_one(Input).clear()
82
+ event.stop()
83
+
84
+ async def on_button_pressed(self, event: Button.Pressed) -> None:
85
+ if not self._pending_approval_data or not self._focused_node_data:
86
+ return
87
+
88
+ is_approved = event.button.id == "approve-btn"
89
+ reason = "User approved via TUI." if is_approved else "User denied via TUI."
90
+
91
+ log_container = self.query_one("#focus-pane-log-container")
92
+ approval_text = "APPROVED" if is_approved else "DENIED"
93
+ display_text = Text(f"{USER_ICON} You: {approval_text} (Reason: {reason})", style="bright_cyan")
94
+ await log_container.mount(Static(""))
95
+ await log_container.mount(Static(display_text))
96
+ log_container.scroll_end(animate=False)
97
+
98
+ self.post_message(self.ApprovalSubmitted(
99
+ agent_name=self._focused_node_data['name'],
100
+ invocation_id=self._pending_approval_data.invocation_id,
101
+ is_approved=is_approved, reason=reason
102
+ ))
103
+ await self._clear_approval_ui()
104
+ event.stop()
105
+
106
+ async def _clear_approval_ui(self):
107
+ self._pending_approval_data = None
108
+ await self.query_one("#approval-buttons").remove_children()
109
+ input_widget = self.query_one(Input)
110
+ if self._focused_node_data and self._focused_node_data.get("type") == "agent":
111
+ input_widget.disabled = False
112
+ input_widget.placeholder = f"Send a message to {self._focused_node_data['name']}..."
113
+ input_widget.focus()
114
+ else:
115
+ input_widget.disabled = True
116
+ input_widget.placeholder = "Select an agent to send messages..."
117
+
118
+ async def _show_approval_prompt(self):
119
+ if not self._pending_approval_data: return
120
+ input_widget = self.query_one(Input)
121
+ input_widget.placeholder = "Please approve or deny the tool call..."
122
+ input_widget.disabled = True
123
+ button_container = self.query_one("#approval-buttons")
124
+ await button_container.remove_children()
125
+ await button_container.mount(
126
+ Button("Approve", variant="success", id="approve-btn"),
127
+ Button("Deny", variant="error", id="deny-btn")
128
+ )
129
+
130
+ def _update_title(self, agent_phases: Dict[str, AgentOperationalPhase], workflow_phases: Dict[str, WorkflowOperationalPhase]):
131
+ """Renders the title of the focus pane with the node's current status."""
132
+ if not self._focused_node_data:
133
+ self.query_one("#focus-pane-title").update("Select a node from the sidebar")
134
+ return
135
+
136
+ node_name = self._focused_node_data.get("name", "Unknown")
137
+ node_type = self._focused_node_data.get("type", "node")
138
+ node_type_str = node_type.replace("_", " ").capitalize()
139
+
140
+ title_icon = DEFAULT_ICON
141
+ phase_str = ""
142
+
143
+ if node_type == 'agent':
144
+ title_icon = AGENT_ICON
145
+ phase = agent_phases.get(node_name, AgentOperationalPhase.UNINITIALIZED)
146
+ phase_str = f" (Status: {phase.value})"
147
+ elif node_type == 'subworkflow':
148
+ title_icon = SUB_WORKFLOW_ICON
149
+ phase = workflow_phases.get(node_name, WorkflowOperationalPhase.UNINITIALIZED)
150
+ phase_str = f" (Status: {phase.value})"
151
+ elif node_type == 'workflow':
152
+ title_icon = WORKFLOW_ICON
153
+ phase = workflow_phases.get(node_name, WorkflowOperationalPhase.UNINITIALIZED)
154
+ phase_str = f" (Status: {phase.value})"
155
+
156
+ self.query_one("#focus-pane-title").update(f"{title_icon} {node_type_str}: [bold]{node_name}[/bold]{phase_str}")
157
+
158
+ def update_current_node_status(self, all_agent_phases: Dict, all_workflow_phases: Dict):
159
+ """A lightweight method to only update the title with the latest status."""
160
+ self._update_title(all_agent_phases, all_workflow_phases)
161
+
162
+ async def update_content(self, node_data: Dict[str, Any], history: List[Any],
163
+ pending_approval: Optional[ToolInvocationApprovalRequestedData],
164
+ all_agent_phases: Dict[str, AgentOperationalPhase],
165
+ all_workflow_phases: Dict[str, WorkflowOperationalPhase]):
166
+ """The main method to update the entire pane based on new state.
167
+ This is called when focus SWITCHES, or when data for a focused workflow is REFRESHED."""
168
+ self.flush_stream_buffers()
169
+
170
+ self._focused_node_data = node_data
171
+ self._pending_approval_data = pending_approval
172
+
173
+ self._update_title(all_agent_phases, all_workflow_phases)
174
+
175
+ log_container = self.query_one("#focus-pane-log-container")
176
+ await log_container.remove_children()
177
+
178
+ # Reset streaming state
179
+ self._thinking_widget = None
180
+ self._thinking_text = None
181
+ self._assistant_content_widget = None
182
+ self._assistant_content_text = None
183
+
184
+ await self._clear_approval_ui()
185
+
186
+ if self._focused_node_data.get("type") == 'agent':
187
+ for event in history:
188
+ await self.add_agent_event(event)
189
+ if self._pending_approval_data:
190
+ await self._show_approval_prompt()
191
+ elif self._focused_node_data.get("type") in ['workflow', 'subworkflow']:
192
+ await self._render_workflow_dashboard(node_data, all_agent_phases, all_workflow_phases)
193
+
194
+ async def _render_workflow_dashboard(self, node_data: Dict[str, Any],
195
+ all_agent_phases: Dict[str, AgentOperationalPhase],
196
+ all_workflow_phases: Dict[str, WorkflowOperationalPhase]):
197
+ """Renders a static summary dashboard for a workflow or sub-workflow."""
198
+ log_container = self.query_one("#focus-pane-log-container")
199
+
200
+ phase = all_workflow_phases.get(node_data['name'], WorkflowOperationalPhase.UNINITIALIZED)
201
+ phase_icon = WORKFLOW_PHASE_ICONS.get(phase, DEFAULT_ICON)
202
+ info_text = Text()
203
+ info_text.append(f"Name: {node_data['name']}\n", style="bold")
204
+ if node_data.get('role'):
205
+ info_text.append(f"Role: {node_data['role']}\n")
206
+ info_text.append(f"Status: {phase_icon} {phase.value}")
207
+ await log_container.mount(Static(Panel(info_text, title="Workflow Info", border_style="green", title_align="left")))
208
+
209
+ children_data = node_data.get("children", {})
210
+ if children_data:
211
+ team_text = Text()
212
+ for name, child_node in children_data.items():
213
+ if child_node['type'] == 'agent':
214
+ agent_phase = all_agent_phases.get(name, AgentOperationalPhase.UNINITIALIZED)
215
+ agent_icon = AGENT_PHASE_ICONS.get(agent_phase, DEFAULT_ICON)
216
+ team_text.append(f" ▪ {agent_icon} {name} (Agent): {agent_phase.value}\n")
217
+ elif child_node['type'] == 'subworkflow':
218
+ wf_phase = all_workflow_phases.get(name, WorkflowOperationalPhase.UNINITIALIZED)
219
+ wf_icon = WORKFLOW_PHASE_ICONS.get(wf_phase, SUB_WORKFLOW_ICON)
220
+ team_text.append(f" ▪ {wf_icon} {name} (Sub-Workflow): {wf_phase.value}\n")
221
+ await log_container.mount(Static(Panel(team_text, title="Team Status", border_style="blue", title_align="left")))
222
+
223
+ async def _close_thinking_block(self, scroll: bool = True):
224
+ """Finalizes and closes the current thinking block if it's open."""
225
+ if self._thinking_widget and self._thinking_text:
226
+ self.flush_stream_buffers() # Ensure any buffered reasoning is flushed first
227
+ self._thinking_text.append("\n</Thinking>", style="dim italic cyan")
228
+ self._thinking_widget.update(self._thinking_text)
229
+ if scroll:
230
+ self.query_one("#focus-pane-log-container").scroll_end(animate=False)
231
+ self._thinking_widget = None
232
+ self._thinking_text = None
233
+
234
+ def flush_stream_buffers(self):
235
+ """Flushes the content of the stream buffers to the UI."""
236
+ scrolled = False
237
+ if self._reasoning_buffer and self._thinking_widget and self._thinking_text:
238
+ self._thinking_text.append(self._reasoning_buffer)
239
+ self._thinking_widget.update(self._thinking_text)
240
+ self._reasoning_buffer = ""
241
+ scrolled = True
242
+
243
+ if self._content_buffer and self._assistant_content_widget and self._assistant_content_text:
244
+ self._assistant_content_text.append(self._content_buffer)
245
+ self._assistant_content_widget.update(self._assistant_content_text)
246
+ self._content_buffer = ""
247
+ scrolled = True
248
+
249
+ if scrolled:
250
+ self.query_one("#focus-pane-log-container").scroll_end(animate=False)
251
+
252
+ async def add_agent_event(self, event: AgentStreamEvent):
253
+ """Adds a single agent event to the log view, handling stream state correctly."""
254
+ log_container = self.query_one("#focus-pane-log-container")
255
+ event_type = event.event_type
256
+
257
+ # Handle streaming content events
258
+ if event_type == AgentStreamEventType.ASSISTANT_CHUNK:
259
+ data: AssistantChunkData = event.data
260
+ if data.reasoning:
261
+ if self._thinking_widget is None:
262
+ self.flush_stream_buffers()
263
+ await log_container.mount(Static(""))
264
+ self._thinking_text = Text("<Thinking>\n", style="dim italic cyan")
265
+ self._thinking_widget = Static(self._thinking_text)
266
+ await log_container.mount(self._thinking_widget)
267
+ self._reasoning_buffer += data.reasoning
268
+
269
+ if data.content:
270
+ if self._thinking_widget:
271
+ await self._close_thinking_block()
272
+ if self._assistant_content_widget is None:
273
+ await log_container.mount(Static(""))
274
+ self._assistant_content_text = Text()
275
+ self._assistant_content_text.append(f"{ASSISTANT_ICON} assistant: ", style="bold green")
276
+ self._assistant_content_widget = Static(self._assistant_content_text)
277
+ await log_container.mount(self._assistant_content_widget)
278
+ self._content_buffer += data.content
279
+ return # This event is handled, do nothing more.
280
+
281
+ # Handle the explicit end of a stream
282
+ if event_type == AgentStreamEventType.ASSISTANT_COMPLETE_RESPONSE:
283
+ was_streaming_content = self._assistant_content_widget is not None
284
+ self.flush_stream_buffers()
285
+ await self._close_thinking_block()
286
+ self._assistant_content_widget = None
287
+ self._assistant_content_text = None
288
+
289
+ # If we weren't streaming, it means this is a non-streamed response. We should render it.
290
+ if not was_streaming_content:
291
+ renderables_list = renderables.render_assistant_complete_response(event.data)
292
+ if renderables_list:
293
+ await log_container.mount(Static(""))
294
+ for item in renderables_list:
295
+ await log_container.mount(Static(item))
296
+ log_container.scroll_end(animate=False)
297
+ return # This event's purpose is to end the stream.
298
+
299
+ # For all other events, first check if they should break an ongoing stream.
300
+ is_stream_breaking_event = event_type in [
301
+ AgentStreamEventType.TOOL_INTERACTION_LOG_ENTRY,
302
+ AgentStreamEventType.TOOL_INVOCATION_AUTO_EXECUTING,
303
+ AgentStreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED,
304
+ AgentStreamEventType.ERROR_EVENT,
305
+ ]
306
+
307
+ if is_stream_breaking_event:
308
+ # Finalize any open assistant block before rendering this event.
309
+ self.flush_stream_buffers()
310
+ await self._close_thinking_block()
311
+ self._assistant_content_widget = None
312
+ self._assistant_content_text = None
313
+
314
+ # Now, render the event if it has a visual representation.
315
+ renderable = None
316
+
317
+ if event_type == AgentStreamEventType.TOOL_INTERACTION_LOG_ENTRY:
318
+ renderable = renderables.render_tool_interaction_log(event.data)
319
+ elif event_type == AgentStreamEventType.TOOL_INVOCATION_AUTO_EXECUTING:
320
+ renderable = renderables.render_tool_auto_executing(event.data)
321
+ elif event_type == AgentStreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED:
322
+ renderable = renderables.render_tool_approval_request(event.data)
323
+ self._pending_approval_data = event.data
324
+ await self._show_approval_prompt()
325
+ elif event_type == AgentStreamEventType.ERROR_EVENT:
326
+ renderable = renderables.render_error(event.data)
327
+ elif event_type in [AgentStreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION, AgentStreamEventType.AGENT_IDLE]:
328
+ # These are informational and do not have a renderable in the log pane.
329
+ pass
330
+
331
+ if renderable:
332
+ await log_container.mount(Static("")) # Add spacer
333
+ await log_container.mount(Static(renderable))
334
+
335
+ log_container.scroll_end(animate=False)
@@ -0,0 +1,27 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/logo.py
2
+ """
3
+ Defines a widget to display the AutoByteus ASCII art logo and tagline.
4
+ """
5
+ from rich.text import Text
6
+ from textual.widgets import Static
7
+ from textual.containers import Vertical
8
+
9
+ class Logo(Vertical):
10
+ """A widget to display the AutoByteus ASCII art logo and tagline."""
11
+
12
+ def compose(self) -> None:
13
+ logo_text = Text(
14
+ """
15
+ _ _ _ _
16
+ / \\ _ _| |_ ___ __| | __| |_
17
+ / _ \\| | | | __/ _ \\/ _` |/ _` | |
18
+ / ___ \\ |_| | || __/ (_| | (_| | |
19
+ /_/ \\_\\__,_|\\__\\___|\\__,_|\\__,_|_|
20
+ """,
21
+ justify="center",
22
+ )
23
+ logo_text.highlight_regex(r"Auto", "bold cyan")
24
+ logo_text.highlight_regex(r"Byteus", "bold magenta")
25
+
26
+ yield Static(logo_text, classes="logo-art")
27
+ yield Static("Orchestrating AI Agent Teams.", classes="logo-tagline")
@@ -0,0 +1,70 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/renderables.py
2
+ """
3
+ Contains pure functions that convert agent event data into Rich renderables for the FocusPane.
4
+ This separates presentation logic from the view logic of the widget itself.
5
+ """
6
+ import json
7
+ from typing import Optional
8
+
9
+ from rich.text import Text
10
+ from rich.panel import Panel
11
+
12
+ from autobyteus.agent.streaming.stream_event_payloads import (
13
+ AgentOperationalPhaseTransitionData, AssistantCompleteResponseData,
14
+ ErrorEventData, ToolInteractionLogEntryData, ToolInvocationApprovalRequestedData, ToolInvocationAutoExecutingData
15
+ )
16
+ from .shared import ASSISTANT_ICON, TOOL_ICON, PROMPT_ICON, ERROR_ICON, LOG_ICON
17
+
18
+ def render_assistant_complete_response(data: AssistantCompleteResponseData) -> list[Text | Panel]:
19
+ """Renders a complete, pre-aggregated assistant response."""
20
+ renderables = []
21
+ if data.reasoning:
22
+ reasoning_text = Text("<Thinking>\n", style="dim italic cyan")
23
+ reasoning_text.append(data.reasoning)
24
+ reasoning_text.append("\n</Thinking>", style="dim italic cyan")
25
+ renderables.append(reasoning_text)
26
+
27
+ if data.content:
28
+ content_text = Text()
29
+ content_text.append(f"{ASSISTANT_ICON} assistant: ", style="bold green")
30
+ content_text.append(data.content)
31
+ renderables.append(content_text)
32
+
33
+ return renderables
34
+
35
+ def render_tool_interaction_log(data: ToolInteractionLogEntryData) -> Text:
36
+ """Renders a tool interaction log entry."""
37
+ return Text(f"{LOG_ICON} [tool-log] {data.log_entry}", style="dim")
38
+
39
+ def render_tool_auto_executing(data: ToolInvocationAutoExecutingData) -> Text:
40
+ """Renders a notification that a tool is being executed automatically."""
41
+ try:
42
+ args_str = json.dumps(data.arguments, indent=2)
43
+ except (TypeError, OverflowError):
44
+ args_str = str(data.arguments)
45
+
46
+ text_content = Text(f"{TOOL_ICON} Executing tool '", style="default")
47
+ text_content.append(f"{data.tool_name}", style="bold yellow")
48
+ text_content.append("' with arguments:\n", style="default")
49
+ text_content.append(args_str, style="yellow")
50
+ return text_content
51
+
52
+ def render_tool_approval_request(data: ToolInvocationApprovalRequestedData) -> Text:
53
+ """Renders a prompt for the user to approve a tool call."""
54
+ try:
55
+ args_str = json.dumps(data.arguments, indent=2)
56
+ except (TypeError, OverflowError):
57
+ args_str = str(data.arguments)
58
+
59
+ text_content = Text(f"{PROMPT_ICON} Requesting approval for tool '", style="default")
60
+ text_content.append(f"{data.tool_name}", style="bold yellow")
61
+ text_content.append("' with arguments:\n", style="default")
62
+ text_content.append(args_str, style="yellow")
63
+ return text_content
64
+
65
+ def render_error(data: ErrorEventData) -> Text:
66
+ """Renders an error event."""
67
+ error_text = f"Error from {data.source}: {data.message}"
68
+ if data.details:
69
+ error_text += f"\nDetails: {data.details}"
70
+ return Text(f"{ERROR_ICON} {error_text}", style="bold red")
@@ -0,0 +1,51 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/shared.py
2
+ """
3
+ Shared constants and data for TUI widgets.
4
+ """
5
+ from typing import Dict
6
+ from autobyteus.agent.phases import AgentOperationalPhase
7
+ from autobyteus.workflow.phases import WorkflowOperationalPhase
8
+
9
+ AGENT_PHASE_ICONS: Dict[AgentOperationalPhase, str] = {
10
+ AgentOperationalPhase.UNINITIALIZED: "⚪",
11
+ AgentOperationalPhase.BOOTSTRAPPING: "⏳",
12
+ AgentOperationalPhase.IDLE: "🟢",
13
+ AgentOperationalPhase.PROCESSING_USER_INPUT: "💭",
14
+ AgentOperationalPhase.AWAITING_LLM_RESPONSE: "💭",
15
+ AgentOperationalPhase.ANALYZING_LLM_RESPONSE: "🤔",
16
+ AgentOperationalPhase.AWAITING_TOOL_APPROVAL: "❓",
17
+ AgentOperationalPhase.TOOL_DENIED: "❌",
18
+ AgentOperationalPhase.EXECUTING_TOOL: "🛠️",
19
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT: "⚙️",
20
+ AgentOperationalPhase.SHUTTING_DOWN: "🌙",
21
+ AgentOperationalPhase.SHUTDOWN_COMPLETE: "⚫",
22
+ AgentOperationalPhase.ERROR: "❗",
23
+ }
24
+
25
+ WORKFLOW_PHASE_ICONS: Dict[WorkflowOperationalPhase, str] = {
26
+ WorkflowOperationalPhase.UNINITIALIZED: "⚪",
27
+ WorkflowOperationalPhase.BOOTSTRAPPING: "⏳",
28
+ WorkflowOperationalPhase.IDLE: "🟢",
29
+ WorkflowOperationalPhase.PROCESSING: "⚙️",
30
+ WorkflowOperationalPhase.SHUTTING_DOWN: "🌙",
31
+ WorkflowOperationalPhase.SHUTDOWN_COMPLETE: "⚫",
32
+ WorkflowOperationalPhase.ERROR: "❗",
33
+ }
34
+
35
+ # Main component icons
36
+ SUB_WORKFLOW_ICON = "📂"
37
+ WORKFLOW_ICON = "🏁"
38
+ AGENT_ICON = "🤖"
39
+
40
+ # General UI icons
41
+ SPEAKING_ICON = "🔊"
42
+ DEFAULT_ICON = "❓"
43
+
44
+ # Semantic icons for log entries
45
+ USER_ICON = "👤"
46
+ ASSISTANT_ICON = "🤖"
47
+ TOOL_ICON = "🛠️"
48
+ PROMPT_ICON = "❓"
49
+ ERROR_ICON = "💥"
50
+ PHASE_ICON = "🔄"
51
+ LOG_ICON = "📄"
@@ -0,0 +1,14 @@
1
+ # file: autobyteus/autobyteus/cli/workflow_tui/widgets/status_bar.py
2
+ """
3
+ Defines the status bar widget for the TUI.
4
+ """
5
+
6
+ from textual.widgets import Footer
7
+
8
+ class StatusBar(Footer):
9
+ """A simple footer widget that displays key bindings."""
10
+
11
+ def __init__(self) -> None:
12
+ super().__init__()
13
+ # This will be automatically populated by Textual's binding system.
14
+ # You can add more status information here if needed in the future.
@@ -42,5 +42,8 @@ class EventType(Enum):
42
42
  # --- Agent Errors (not necessarily phase changes, e.g., error during output generation) ---
43
43
  AGENT_ERROR_OUTPUT_GENERATION = "agent_error_output_generation"
44
44
 
45
+ # --- Workflow Events ---
46
+ WORKFLOW_STREAM_EVENT = "workflow_stream_event" # For unified workflow event stream
47
+
45
48
  def __str__(self):
46
49
  return self.value