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,42 @@
1
+ # file: autobyteus/autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.workflow.shutdown_steps.base_workflow_shutdown_step import BaseWorkflowShutdownStep
7
+
8
+ if TYPE_CHECKING:
9
+ from autobyteus.workflow.context.workflow_context import WorkflowContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class AgentTeamShutdownStep(BaseWorkflowShutdownStep):
14
+ """Shutdown step to gracefully stop all running agents in the workflow."""
15
+ async def execute(self, context: 'WorkflowContext') -> bool:
16
+ workflow_id = context.workflow_id
17
+ logger.info(f"Workflow '{workflow_id}': Executing AgentTeamShutdownStep.")
18
+
19
+ team_manager = context.team_manager
20
+ if not team_manager:
21
+ logger.warning(f"Workflow '{workflow_id}': No TeamManager found, cannot shut down agents.")
22
+ return True
23
+
24
+ # Get the list of all created agents from the single source of truth.
25
+ all_agents = team_manager.get_all_agents()
26
+ running_agents = [agent for agent in all_agents if agent.is_running]
27
+
28
+ if not running_agents:
29
+ logger.info(f"Workflow '{workflow_id}': No running agents to shut down.")
30
+ return True
31
+
32
+ logger.info(f"Workflow '{workflow_id}': Shutting down {len(running_agents)} running agents.")
33
+ stop_tasks = [agent.stop(timeout=10.0) for agent in running_agents]
34
+ results = await asyncio.gather(*stop_tasks, return_exceptions=True)
35
+
36
+ all_successful = True
37
+ for agent, result in zip(running_agents, results):
38
+ if isinstance(result, Exception):
39
+ logger.error(f"Workflow '{workflow_id}': Error stopping agent '{agent.agent_id}': {result}", exc_info=result)
40
+ all_successful = False
41
+
42
+ return all_successful
@@ -0,0 +1,16 @@
1
+ # file: autobyteus/autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py
2
+ import logging
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from autobyteus.workflow.context.workflow_context import WorkflowContext
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class BaseWorkflowShutdownStep(ABC):
12
+ """Abstract base class for individual steps in the workflow shutdown process."""
13
+ @abstractmethod
14
+ async def execute(self, context: 'WorkflowContext') -> bool:
15
+ """Executes the shutdown step."""
16
+ raise NotImplementedError("Subclasses must implement the 'execute' method.")
@@ -0,0 +1,28 @@
1
+ # file: autobyteus/autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py
2
+ import logging
3
+ from typing import TYPE_CHECKING
4
+
5
+ from autobyteus.workflow.shutdown_steps.base_workflow_shutdown_step import BaseWorkflowShutdownStep
6
+
7
+ if TYPE_CHECKING:
8
+ from autobyteus.workflow.context.workflow_context import WorkflowContext
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class BridgeCleanupStep(BaseWorkflowShutdownStep):
13
+ """Shutdown step to gracefully stop all AgentEventBridge instances via the multiplexer."""
14
+ async def execute(self, context: 'WorkflowContext') -> bool:
15
+ workflow_id = context.workflow_id
16
+ logger.info(f"Workflow '{workflow_id}': Executing BridgeCleanupStep.")
17
+
18
+ multiplexer = context.multiplexer
19
+ if not multiplexer:
20
+ logger.warning(f"Workflow '{workflow_id}': No AgentEventMultiplexer found, cannot shut down event bridges.")
21
+ return True
22
+
23
+ try:
24
+ await multiplexer.shutdown()
25
+ return True
26
+ except Exception as e:
27
+ logger.error(f"Workflow '{workflow_id}': Error shutting down agent event bridges via multiplexer: {e}", exc_info=True)
28
+ return False
@@ -0,0 +1,41 @@
1
+ # file: autobyteus/autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.workflow.shutdown_steps.base_workflow_shutdown_step import BaseWorkflowShutdownStep
7
+
8
+ if TYPE_CHECKING:
9
+ from autobyteus.workflow.context.workflow_context import WorkflowContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class SubWorkflowShutdownStep(BaseWorkflowShutdownStep):
14
+ """Shutdown step to gracefully stop all running sub-workflows."""
15
+ async def execute(self, context: 'WorkflowContext') -> bool:
16
+ workflow_id = context.workflow_id
17
+ logger.info(f"Workflow '{workflow_id}': Executing SubWorkflowShutdownStep.")
18
+
19
+ team_manager = context.team_manager
20
+ if not team_manager:
21
+ logger.warning(f"Workflow '{workflow_id}': No TeamManager found, cannot shut down sub-workflows.")
22
+ return True
23
+
24
+ all_sub_workflows = team_manager.get_all_sub_workflows()
25
+ running_sub_workflows = [wf for wf in all_sub_workflows if wf.is_running]
26
+
27
+ if not running_sub_workflows:
28
+ logger.info(f"Workflow '{workflow_id}': No running sub-workflows to shut down.")
29
+ return True
30
+
31
+ logger.info(f"Workflow '{workflow_id}': Shutting down {len(running_sub_workflows)} running sub-workflows.")
32
+ stop_tasks = [wf.stop(timeout=20.0) for wf in running_sub_workflows]
33
+ results = await asyncio.gather(*stop_tasks, return_exceptions=True)
34
+
35
+ all_successful = True
36
+ for wf, result in zip(running_sub_workflows, results):
37
+ if isinstance(result, Exception):
38
+ logger.error(f"Workflow '{workflow_id}': Error stopping sub-workflow '{wf.name}': {result}", exc_info=result)
39
+ all_successful = False
40
+
41
+ return all_successful
@@ -0,0 +1,35 @@
1
+ # file: autobyteus/autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py
2
+ import logging
3
+ from typing import TYPE_CHECKING, List, Optional
4
+
5
+ from autobyteus.workflow.shutdown_steps.base_workflow_shutdown_step import BaseWorkflowShutdownStep
6
+ from autobyteus.workflow.shutdown_steps.bridge_cleanup_step import BridgeCleanupStep
7
+ from autobyteus.workflow.shutdown_steps.sub_workflow_shutdown_step import SubWorkflowShutdownStep
8
+ from autobyteus.workflow.shutdown_steps.agent_team_shutdown_step import AgentTeamShutdownStep
9
+
10
+ if TYPE_CHECKING:
11
+ from autobyteus.workflow.context.workflow_context import WorkflowContext
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class WorkflowShutdownOrchestrator:
16
+ """Orchestrates the workflow's shutdown process."""
17
+ def __init__(self, steps: Optional[List[BaseWorkflowShutdownStep]] = None):
18
+ self.shutdown_steps = steps or [
19
+ BridgeCleanupStep(),
20
+ SubWorkflowShutdownStep(),
21
+ AgentTeamShutdownStep(),
22
+ ]
23
+
24
+ async def run(self, context: 'WorkflowContext') -> bool:
25
+ workflow_id = context.workflow_id
26
+ logger.info(f"Workflow '{workflow_id}': Shutdown orchestrator starting.")
27
+
28
+ all_successful = True
29
+ for step in self.shutdown_steps:
30
+ if not await step.execute(context):
31
+ logger.error(f"Workflow '{workflow_id}': Shutdown step {step.__class__.__name__} failed.")
32
+ all_successful = False
33
+
34
+ logger.info(f"Workflow '{workflow_id}': Shutdown orchestration completed.")
35
+ return all_successful
@@ -0,0 +1,26 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/__init__.py
2
+ """
3
+ Components related to workflow output streaming.
4
+ """
5
+ from .workflow_event_notifier import WorkflowExternalEventNotifier
6
+ from .workflow_event_stream import WorkflowEventStream
7
+ from .workflow_stream_events import WorkflowStreamEvent, WorkflowStreamDataPayload
8
+ from .workflow_stream_event_payloads import (
9
+ BaseWorkflowSpecificPayload,
10
+ WorkflowPhaseTransitionData,
11
+ AgentEventRebroadcastPayload,
12
+ )
13
+ from .agent_event_bridge import AgentEventBridge
14
+ from .agent_event_multiplexer import AgentEventMultiplexer
15
+
16
+ __all__ = [
17
+ "WorkflowExternalEventNotifier",
18
+ "WorkflowEventStream",
19
+ "WorkflowStreamEvent",
20
+ "WorkflowStreamDataPayload",
21
+ "BaseWorkflowSpecificPayload",
22
+ "WorkflowPhaseTransitionData",
23
+ "AgentEventRebroadcastPayload",
24
+ "AgentEventBridge",
25
+ "AgentEventMultiplexer",
26
+ ]
@@ -0,0 +1,48 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/agent_event_bridge.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.agent.streaming.agent_event_stream import AgentEventStream
7
+
8
+ if TYPE_CHECKING:
9
+ from autobyteus.agent.agent import Agent
10
+ from autobyteus.workflow.streaming.workflow_event_notifier import WorkflowExternalEventNotifier
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class AgentEventBridge:
15
+ """
16
+ A dedicated component that bridges events from a single agent's event stream
17
+ to the main workflow notifier. This is the core of the multiplexing pattern.
18
+ """
19
+ def __init__(self, agent: 'Agent', agent_name: str, notifier: 'WorkflowExternalEventNotifier', loop: asyncio.AbstractEventLoop):
20
+ self._agent_name = agent_name
21
+ self._notifier = notifier
22
+ self._stream = AgentEventStream(agent)
23
+ self._task: asyncio.Task = loop.create_task(self._run())
24
+ logger.info(f"AgentEventBridge created and task started for agent '{agent_name}'.")
25
+
26
+ async def _run(self):
27
+ """The background task that consumes from the stream and publishes to the notifier."""
28
+ try:
29
+ async for event in self._stream.all_events():
30
+ self._notifier.publish_agent_event(self._agent_name, event)
31
+ except asyncio.CancelledError:
32
+ logger.info(f"AgentEventBridge task for '{self._agent_name}' was cancelled.")
33
+ except Exception as e:
34
+ logger.error(f"Error in AgentEventBridge for '{self._agent_name}': {e}", exc_info=True)
35
+ finally:
36
+ logger.debug(f"AgentEventBridge task for '{self._agent_name}' is finishing.")
37
+
38
+ async def cancel(self):
39
+ """Gracefully stops the bridge."""
40
+ logger.info(f"Cancelling AgentEventBridge for '{self._agent_name}'.")
41
+ if not self._task.done():
42
+ self._task.cancel()
43
+ try:
44
+ await self._task
45
+ except asyncio.CancelledError:
46
+ pass # Expected
47
+ await self._stream.close()
48
+ logger.info(f"AgentEventBridge for '{self._agent_name}' cancelled successfully.")
@@ -0,0 +1,70 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/agent_event_multiplexer.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING, Dict, Optional
5
+
6
+ from autobyteus.workflow.streaming.agent_event_bridge import AgentEventBridge
7
+ from autobyteus.workflow.streaming.workflow_event_bridge import WorkflowEventBridge
8
+
9
+ if TYPE_CHECKING:
10
+ from autobyteus.agent.agent import Agent
11
+ from autobyteus.workflow.agentic_workflow import AgenticWorkflow
12
+ from autobyteus.workflow.streaming.workflow_event_notifier import WorkflowExternalEventNotifier
13
+ from autobyteus.workflow.runtime.workflow_worker import WorkflowWorker
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class AgentEventMultiplexer:
18
+ """
19
+ Manages the lifecycle of event bridges for all nodes (agents and sub-workflows).
20
+ It creates, tracks, and shuts down the bridges that forward node events
21
+ to the workflow's main event stream.
22
+ """
23
+ def __init__(self, workflow_id: str, notifier: 'WorkflowExternalEventNotifier', worker_ref: 'WorkflowWorker'):
24
+ self._workflow_id = workflow_id
25
+ self._notifier = notifier
26
+ self._worker = worker_ref
27
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
28
+ self._agent_bridges: Dict[str, AgentEventBridge] = {}
29
+ self._workflow_bridges: Dict[str, WorkflowEventBridge] = {}
30
+ logger.info(f"AgentEventMultiplexer initialized for workflow '{self._workflow_id}'.")
31
+
32
+ def _get_loop(self) -> asyncio.AbstractEventLoop:
33
+ """Retrieves the event loop from the worker on-demand."""
34
+ if self._loop is None or self._loop.is_closed():
35
+ self._loop = self._worker.get_worker_loop()
36
+ if self._loop is None:
37
+ raise RuntimeError(f"Workflow worker loop for workflow '{self._workflow_id}' is not available or not running.")
38
+ return self._loop
39
+
40
+ def start_bridging_agent_events(self, agent: 'Agent', agent_name: str):
41
+ """Creates and starts an AgentEventBridge for a direct agent node."""
42
+ if agent_name in self._agent_bridges:
43
+ logger.warning(f"Event bridge for agent '{agent_name}' already exists. Skipping creation.")
44
+ return
45
+
46
+ bridge = AgentEventBridge(agent=agent, agent_name=agent_name, notifier=self._notifier, loop=self._get_loop())
47
+ self._agent_bridges[agent_name] = bridge
48
+ logger.info(f"AgentEventMultiplexer started agent event bridge for '{agent_name}'.")
49
+
50
+ def start_bridging_workflow_events(self, sub_workflow: 'AgenticWorkflow', node_name: str):
51
+ """Creates and starts a WorkflowEventBridge for a sub-workflow node."""
52
+ if node_name in self._workflow_bridges:
53
+ logger.warning(f"Event bridge for sub-workflow '{node_name}' already exists. Skipping creation.")
54
+ return
55
+
56
+ bridge = WorkflowEventBridge(sub_workflow=sub_workflow, sub_workflow_node_name=node_name, parent_notifier=self._notifier, loop=self._get_loop())
57
+ self._workflow_bridges[node_name] = bridge
58
+ logger.info(f"AgentEventMultiplexer started workflow event bridge for '{node_name}'.")
59
+
60
+ async def shutdown(self):
61
+ """Gracefully shuts down all active event bridges."""
62
+ logger.info(f"AgentEventMultiplexer for '{self._workflow_id}' shutting down all event bridges.")
63
+ agent_bridge_tasks = [b.cancel() for b in self._agent_bridges.values()]
64
+ workflow_bridge_tasks = [b.cancel() for b in self._workflow_bridges.values()]
65
+
66
+ await asyncio.gather(*(agent_bridge_tasks + workflow_bridge_tasks), return_exceptions=True)
67
+
68
+ self._agent_bridges.clear()
69
+ self._workflow_bridges.clear()
70
+ logger.info(f"All event bridges for workflow '{self._workflow_id}' have been shut down by multiplexer.")
@@ -0,0 +1,50 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/workflow_event_bridge.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.workflow.streaming.workflow_event_stream import WorkflowEventStream
7
+
8
+ if TYPE_CHECKING:
9
+ from autobyteus.workflow.agentic_workflow import AgenticWorkflow
10
+ from autobyteus.workflow.streaming.workflow_event_notifier import WorkflowExternalEventNotifier
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class WorkflowEventBridge:
15
+ """
16
+ A dedicated component that bridges events from a sub-workflow's event stream
17
+ to the parent workflow's notifier.
18
+ """
19
+ def __init__(self, sub_workflow: 'AgenticWorkflow', sub_workflow_node_name: str, parent_notifier: 'WorkflowExternalEventNotifier', loop: asyncio.AbstractEventLoop):
20
+ self._sub_workflow = sub_workflow
21
+ self._sub_workflow_node_name = sub_workflow_node_name
22
+ self._parent_notifier = parent_notifier
23
+ self._stream = WorkflowEventStream(sub_workflow)
24
+ self._task: asyncio.Task = loop.create_task(self._run())
25
+ logger.info(f"WorkflowEventBridge created and task started for sub-workflow '{sub_workflow_node_name}'.")
26
+
27
+ async def _run(self):
28
+ """The background task that consumes from the sub-workflow stream and re-publishes."""
29
+ try:
30
+ async for event in self._stream.all_events():
31
+ # Re-broadcast the event to the parent, adding the sub-workflow context.
32
+ self._parent_notifier.publish_sub_workflow_event(self._sub_workflow_node_name, event)
33
+ except asyncio.CancelledError:
34
+ logger.info(f"WorkflowEventBridge task for '{self._sub_workflow_node_name}' was cancelled.")
35
+ except Exception as e:
36
+ logger.error(f"Error in WorkflowEventBridge for '{self._sub_workflow_node_name}': {e}", exc_info=True)
37
+ finally:
38
+ logger.debug(f"WorkflowEventBridge task for '{self._sub_workflow_node_name}' is finishing.")
39
+
40
+ async def cancel(self):
41
+ """Gracefully stops the bridge."""
42
+ logger.info(f"Cancelling WorkflowEventBridge for '{self._sub_workflow_node_name}'.")
43
+ if not self._task.done():
44
+ self._task.cancel()
45
+ try:
46
+ await self._task
47
+ except asyncio.CancelledError:
48
+ pass # Expected
49
+ await self._stream.close()
50
+ logger.info(f"WorkflowEventBridge for '{self._sub_workflow_node_name}' cancelled successfully.")
@@ -0,0 +1,83 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/workflow_event_notifier.py
2
+ import logging
3
+ from typing import Optional, Dict, Any, TYPE_CHECKING
4
+
5
+ from autobyteus.events.event_emitter import EventEmitter
6
+ from autobyteus.events.event_types import EventType
7
+ from autobyteus.workflow.phases.workflow_operational_phase import WorkflowOperationalPhase
8
+ from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent
9
+ from .workflow_stream_events import WorkflowStreamEvent, AgentEventRebroadcastPayload, WorkflowPhaseTransitionData, SubWorkflowEventRebroadcastPayload
10
+
11
+ if TYPE_CHECKING:
12
+ from autobyteus.workflow.runtime.workflow_runtime import WorkflowRuntime
13
+ from autobyteus.workflow.streaming.workflow_stream_events import WorkflowStreamEvent as WorkflowStreamEventTypeHint
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class WorkflowExternalEventNotifier(EventEmitter):
18
+ """
19
+ Responsible for emitting unified WorkflowStreamEvents for consumption by
20
+ external listeners (like a UI or the WorkflowEventStream).
21
+ """
22
+ def __init__(self, workflow_id: str, runtime_ref: 'WorkflowRuntime'):
23
+ super().__init__()
24
+ self.workflow_id = workflow_id
25
+ self.runtime_ref = runtime_ref
26
+ logger.debug(f"WorkflowExternalEventNotifier initialized for workflow '{self.workflow_id}'.")
27
+
28
+ def _emit_event(self, event: 'WorkflowStreamEventTypeHint'):
29
+ """
30
+ Emits a fully-formed WorkflowStreamEvent.
31
+ A new generic event type is used for the underlying pub/sub system to carry
32
+ the unified event object.
33
+ """
34
+ self.emit(EventType.WORKFLOW_STREAM_EVENT, payload=event)
35
+
36
+ def notify_phase_change(self, new_phase: WorkflowOperationalPhase, old_phase: Optional[WorkflowOperationalPhase], extra_data: Optional[Dict[str, Any]] = None):
37
+ """
38
+ Notifies of a workflow phase transition by creating and emitting a
39
+ 'WORKFLOW' sourced event.
40
+ """
41
+ payload_dict = {
42
+ "new_phase": new_phase,
43
+ "old_phase": old_phase,
44
+ "error_message": extra_data.get("error_message") if extra_data else None,
45
+ }
46
+ filtered_payload_dict = {k: v for k, v in payload_dict.items() if v is not None}
47
+
48
+ event = WorkflowStreamEvent(
49
+ workflow_id=self.workflow_id,
50
+ event_source_type="WORKFLOW",
51
+ data=WorkflowPhaseTransitionData(**filtered_payload_dict)
52
+ )
53
+ self._emit_event(event)
54
+
55
+ def publish_agent_event(self, agent_name: str, agent_event: AgentStreamEvent):
56
+ """
57
+ Wraps an event from a direct member agent and publishes it on the main workflow stream
58
+ as an 'AGENT' sourced event.
59
+ """
60
+ event = WorkflowStreamEvent(
61
+ workflow_id=self.workflow_id,
62
+ event_source_type="AGENT",
63
+ data=AgentEventRebroadcastPayload(
64
+ agent_name=agent_name,
65
+ agent_event=agent_event
66
+ )
67
+ )
68
+ self._emit_event(event)
69
+
70
+ def publish_sub_workflow_event(self, sub_workflow_node_name: str, sub_workflow_event: 'WorkflowStreamEventTypeHint'):
71
+ """
72
+ Wraps an event from a sub-workflow and publishes it on the parent workflow stream
73
+ as a 'SUB_WORKFLOW' sourced event.
74
+ """
75
+ event = WorkflowStreamEvent(
76
+ workflow_id=self.workflow_id,
77
+ event_source_type="SUB_WORKFLOW",
78
+ data=SubWorkflowEventRebroadcastPayload(
79
+ sub_workflow_node_name=sub_workflow_node_name,
80
+ sub_workflow_event=sub_workflow_event
81
+ )
82
+ )
83
+ self._emit_event(event)
@@ -0,0 +1,33 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/workflow_event_stream.py
2
+ import asyncio
3
+ import queue
4
+ from typing import AsyncIterator, TYPE_CHECKING
5
+
6
+ from autobyteus.events.event_types import EventType
7
+ from autobyteus.workflow.streaming.workflow_stream_events import WorkflowStreamEvent
8
+ from autobyteus.agent.streaming.queue_streamer import stream_queue_items
9
+
10
+ if TYPE_CHECKING:
11
+ from autobyteus.workflow.agentic_workflow import AgenticWorkflow
12
+
13
+ _SENTINEL = object()
14
+
15
+ class WorkflowEventStream:
16
+ """Consumes events from a WorkflowExternalEventNotifier for a specific workflow."""
17
+ def __init__(self, workflow: 'AgenticWorkflow'):
18
+ self.workflow_id = workflow.workflow_id
19
+ self._internal_q = queue.Queue()
20
+ self._notifier = workflow._runtime.notifier
21
+ self._notifier.subscribe(EventType.WORKFLOW_STREAM_EVENT, self._handle_event)
22
+
23
+ def _handle_event(self, payload: WorkflowStreamEvent, **kwargs):
24
+ if isinstance(payload, WorkflowStreamEvent) and payload.workflow_id == self.workflow_id:
25
+ self._internal_q.put(payload)
26
+
27
+ async def close(self):
28
+ self._notifier.unsubscribe(EventType.WORKFLOW_STREAM_EVENT, self._handle_event)
29
+ await asyncio.get_running_loop().run_in_executor(None, self._internal_q.put, _SENTINEL)
30
+
31
+ def all_events(self) -> AsyncIterator[WorkflowStreamEvent]:
32
+ """The primary method to consume all structured events from the workflow."""
33
+ return stream_queue_items(self._internal_q, _SENTINEL, f"workflow_{self.workflow_id}_stream")
@@ -0,0 +1,28 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/workflow_stream_event_payloads.py
2
+ from typing import Optional, Any
3
+ from pydantic import BaseModel, Field
4
+ from autobyteus.workflow.phases.workflow_operational_phase import WorkflowOperationalPhase
5
+ from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent
6
+ # Need to use a forward reference string to avoid circular import at runtime
7
+ from typing import TYPE_CHECKING
8
+ if TYPE_CHECKING:
9
+ from autobyteus.workflow.streaming.workflow_stream_events import WorkflowStreamEvent
10
+
11
+ # --- Payloads for events originating from the "WORKFLOW" source ---
12
+ class BaseWorkflowSpecificPayload(BaseModel):
13
+ pass
14
+
15
+ class WorkflowPhaseTransitionData(BaseWorkflowSpecificPayload):
16
+ new_phase: WorkflowOperationalPhase
17
+ old_phase: Optional[WorkflowOperationalPhase] = None
18
+ error_message: Optional[str] = None
19
+
20
+ # --- Payload for events originating from the "AGENT" source ---
21
+ class AgentEventRebroadcastPayload(BaseModel):
22
+ agent_name: str # The friendly name, e.g., "Researcher_1"
23
+ agent_event: AgentStreamEvent # The original, unmodified event from the agent
24
+
25
+ # --- Payload for events originating from the "SUB_WORKFLOW" source ---
26
+ class SubWorkflowEventRebroadcastPayload(BaseModel):
27
+ sub_workflow_node_name: str # The friendly name of the sub-workflow node
28
+ sub_workflow_event: "WorkflowStreamEvent" = Field(..., description="The original, unmodified event from the sub-workflow's stream")
@@ -0,0 +1,45 @@
1
+ # file: autobyteus/autobyteus/workflow/streaming/workflow_stream_events.py
2
+ import datetime
3
+ import uuid
4
+ from typing import Literal, Union
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+ from .workflow_stream_event_payloads import WorkflowPhaseTransitionData, AgentEventRebroadcastPayload, SubWorkflowEventRebroadcastPayload
8
+
9
+ # A union of all possible payloads for a "WORKFLOW" sourced event.
10
+ WorkflowSpecificPayload = Union[WorkflowPhaseTransitionData]
11
+
12
+ # The top-level discriminated union for the main event stream's payload.
13
+ WorkflowStreamDataPayload = Union[WorkflowSpecificPayload, AgentEventRebroadcastPayload, SubWorkflowEventRebroadcastPayload]
14
+
15
+ class WorkflowStreamEvent(BaseModel):
16
+ event_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
17
+ timestamp: datetime.datetime = Field(default_factory=datetime.datetime.utcnow)
18
+ workflow_id: str
19
+ event_source_type: Literal["WORKFLOW", "AGENT", "SUB_WORKFLOW"]
20
+ data: WorkflowStreamDataPayload
21
+
22
+ @model_validator(mode='after')
23
+ def check_data_matches_source_type(self) -> 'WorkflowStreamEvent':
24
+ is_agent_event = self.event_source_type == "AGENT"
25
+ is_agent_payload = isinstance(self.data, AgentEventRebroadcastPayload)
26
+
27
+ is_sub_workflow_event = self.event_source_type == "SUB_WORKFLOW"
28
+ is_sub_workflow_payload = isinstance(self.data, SubWorkflowEventRebroadcastPayload)
29
+
30
+ is_workflow_event = self.event_source_type == "WORKFLOW"
31
+ is_workflow_payload = isinstance(self.data, WorkflowPhaseTransitionData)
32
+
33
+ if is_agent_event and not is_agent_payload:
34
+ raise ValueError("event_source_type is 'AGENT' but data is not an AgentEventRebroadcastPayload")
35
+
36
+ if is_sub_workflow_event and not is_sub_workflow_payload:
37
+ raise ValueError("event_source_type is 'SUB_WORKFLOW' but data is not a SubWorkflowEventRebroadcastPayload")
38
+
39
+ if is_workflow_event and not is_workflow_payload:
40
+ raise ValueError("event_source_type is 'WORKFLOW' but data is not a valid workflow-specific payload")
41
+
42
+ return self
43
+
44
+ # This is necessary for Pydantic v2 to correctly handle the recursive model
45
+ SubWorkflowEventRebroadcastPayload.model_rebuild()
@@ -0,0 +1,9 @@
1
+ # file: autobyteus/autobyteus/workflow/utils/__init__.py
2
+ """
3
+ Utility functions for interacting with workflows.
4
+ """
5
+ from .wait_for_idle import wait_for_workflow_to_be_idle
6
+
7
+ __all__ = [
8
+ "wait_for_workflow_to_be_idle",
9
+ ]
@@ -0,0 +1,46 @@
1
+ # file: autobyteus/autobyteus/workflow/utils/wait_for_idle.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from autobyteus.workflow.streaming.workflow_event_stream import WorkflowEventStream
7
+ from autobyteus.workflow.phases.workflow_operational_phase import WorkflowOperationalPhase
8
+
9
+ if TYPE_CHECKING:
10
+ from autobyteus.workflow.agentic_workflow import AgenticWorkflow
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ async def _wait_loop(streamer: WorkflowEventStream, workflow_id: str):
15
+ """Internal helper to listen for the IDLE or ERROR event."""
16
+ async for event in streamer.all_events():
17
+ if event.event_source_type == "WORKFLOW" and event.data.new_phase == WorkflowOperationalPhase.IDLE:
18
+ logger.info(f"Workflow '{workflow_id}' has become idle.")
19
+ return
20
+ if event.event_source_type == "WORKFLOW" and event.data.new_phase == WorkflowOperationalPhase.ERROR:
21
+ error_message = f"Workflow '{workflow_id}' entered an error state while waiting for idle: {event.data.error_message}"
22
+ logger.error(error_message)
23
+ raise RuntimeError(error_message)
24
+
25
+ async def wait_for_workflow_to_be_idle(workflow: 'AgenticWorkflow', timeout: float = 60.0):
26
+ """
27
+ Waits for a workflow to complete its bootstrapping and enter the IDLE state.
28
+
29
+ Args:
30
+ workflow: The workflow instance to monitor.
31
+ timeout: The maximum time in seconds to wait.
32
+
33
+ Raises:
34
+ asyncio.TimeoutError: If the workflow does not become idle within the timeout period.
35
+ RuntimeError: If the workflow enters an error state.
36
+ """
37
+ if workflow.get_current_phase() == WorkflowOperationalPhase.IDLE:
38
+ return
39
+
40
+ logger.info(f"Waiting for workflow '{workflow.workflow_id}' to become idle (timeout: {timeout}s)...")
41
+
42
+ streamer = WorkflowEventStream(workflow)
43
+ try:
44
+ await asyncio.wait_for(_wait_loop(streamer, workflow.workflow_id), timeout=timeout)
45
+ finally:
46
+ await streamer.close()