autobyteus 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autobyteus/agent/agent.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
- autobyteus/agent/context/__init__.py +4 -2
- autobyteus/agent/context/agent_config.py +35 -8
- autobyteus/agent/context/agent_context_registry.py +73 -0
- autobyteus/agent/events/notifiers.py +4 -0
- autobyteus/agent/events/worker_event_dispatcher.py +1 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +8 -3
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
- autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
- autobyteus/agent/handlers/user_input_message_event_handler.py +16 -1
- autobyteus/agent/input_processor/__init__.py +1 -7
- autobyteus/agent/message/context_file_type.py +6 -0
- autobyteus/agent/message/send_message_to.py +74 -99
- autobyteus/agent/phases/discover.py +2 -1
- autobyteus/agent/runtime/agent_runtime.py +10 -2
- autobyteus/agent/runtime/agent_worker.py +1 -0
- autobyteus/agent/sender_type.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +6 -0
- autobyteus/agent/streaming/stream_event_payloads.py +12 -0
- autobyteus/agent/streaming/stream_events.py +3 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
- autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
- autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
- autobyteus/agent/workspace/base_workspace.py +17 -2
- autobyteus/agent_team/__init__.py +1 -0
- autobyteus/agent_team/agent_team.py +93 -0
- autobyteus/agent_team/agent_team_builder.py +184 -0
- autobyteus/agent_team/base_agent_team.py +86 -0
- autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
- autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
- autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
- autobyteus/agent_team/context/__init__.py +17 -0
- autobyteus/agent_team/context/agent_team_config.py +33 -0
- autobyteus/agent_team/context/agent_team_context.py +61 -0
- autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
- autobyteus/agent_team/context/team_manager.py +147 -0
- autobyteus/agent_team/context/team_node_config.py +76 -0
- autobyteus/agent_team/events/__init__.py +29 -0
- autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
- autobyteus/agent_team/events/agent_team_events.py +53 -0
- autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
- autobyteus/agent_team/exceptions.py +8 -0
- autobyteus/agent_team/factory/__init__.py +9 -0
- autobyteus/agent_team/factory/agent_team_factory.py +99 -0
- autobyteus/agent_team/handlers/__init__.py +19 -0
- autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
- autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
- autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
- autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
- autobyteus/agent_team/phases/__init__.py +11 -0
- autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
- autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
- autobyteus/agent_team/runtime/__init__.py +13 -0
- autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
- autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
- autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
- autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
- autobyteus/agent_team/streaming/__init__.py +26 -0
- autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
- autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
- autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
- autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
- autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
- autobyteus/agent_team/task_notification/__init__.py +11 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
- autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
- autobyteus/agent_team/utils/__init__.py +9 -0
- autobyteus/agent_team/utils/wait_for_idle.py +46 -0
- autobyteus/cli/__init__.py +1 -1
- autobyteus/cli/agent_team_tui/__init__.py +4 -0
- autobyteus/cli/agent_team_tui/app.py +210 -0
- autobyteus/cli/agent_team_tui/state.py +180 -0
- autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
- autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
- autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
- autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
- autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
- autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
- autobyteus/cli/cli_display.py +1 -1
- autobyteus/cli/workflow_tui/__init__.py +4 -0
- autobyteus/cli/workflow_tui/app.py +210 -0
- autobyteus/cli/workflow_tui/state.py +189 -0
- autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
- autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
- autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
- autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
- autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
- autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
- autobyteus/events/event_types.py +8 -0
- autobyteus/llm/api/autobyteus_llm.py +11 -12
- autobyteus/llm/api/lmstudio_llm.py +34 -0
- autobyteus/llm/api/ollama_llm.py +8 -13
- autobyteus/llm/api/openai_compatible_llm.py +20 -3
- autobyteus/llm/autobyteus_provider.py +73 -46
- autobyteus/llm/llm_factory.py +103 -139
- autobyteus/llm/lmstudio_provider.py +104 -0
- autobyteus/llm/models.py +83 -53
- autobyteus/llm/ollama_provider.py +69 -61
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +13 -12
- autobyteus/llm/runtimes.py +11 -0
- autobyteus/llm/token_counter/token_counter_factory.py +2 -0
- autobyteus/task_management/__init__.py +43 -0
- autobyteus/task_management/base_task_board.py +68 -0
- autobyteus/task_management/converters/__init__.py +11 -0
- autobyteus/task_management/converters/task_board_converter.py +64 -0
- autobyteus/task_management/converters/task_plan_converter.py +48 -0
- autobyteus/task_management/deliverable.py +16 -0
- autobyteus/task_management/deliverables/__init__.py +8 -0
- autobyteus/task_management/deliverables/file_deliverable.py +15 -0
- autobyteus/task_management/events.py +27 -0
- autobyteus/task_management/in_memory_task_board.py +126 -0
- autobyteus/task_management/schemas/__init__.py +15 -0
- autobyteus/task_management/schemas/deliverable_schema.py +13 -0
- autobyteus/task_management/schemas/plan_definition.py +35 -0
- autobyteus/task_management/schemas/task_status_report.py +27 -0
- autobyteus/task_management/task_plan.py +110 -0
- autobyteus/task_management/tools/__init__.py +14 -0
- autobyteus/task_management/tools/get_task_board_status.py +68 -0
- autobyteus/task_management/tools/publish_task_plan.py +113 -0
- autobyteus/task_management/tools/update_task_status.py +135 -0
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/ask_user_input.py +2 -1
- autobyteus/tools/bash/bash_executor.py +61 -15
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
- autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
- autobyteus/tools/browser/standalone/navigate_to.py +2 -0
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
- autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
- autobyteus/tools/file/file_reader.py +36 -9
- autobyteus/tools/file/file_writer.py +37 -9
- autobyteus/tools/functional_tool.py +5 -4
- autobyteus/tools/image_downloader.py +2 -0
- autobyteus/tools/mcp/config_service.py +63 -58
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server_instance_manager.py +30 -4
- autobyteus/tools/mcp/tool_registrar.py +106 -51
- autobyteus/tools/parameter_schema.py +17 -11
- autobyteus/tools/pdf_downloader.py +2 -1
- autobyteus/tools/registry/tool_definition.py +36 -37
- autobyteus/tools/registry/tool_registry.py +50 -2
- autobyteus/tools/timer.py +2 -0
- autobyteus/tools/tool_category.py +15 -4
- autobyteus/tools/tool_meta.py +6 -1
- autobyteus/tools/tool_origin.py +10 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
- autobyteus/tools/usage/providers/__init__.py +2 -12
- autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
- autobyteus/tools/usage/registries/__init__.py +7 -12
- autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
- autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
- autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
- autobyteus/workflow/agentic_workflow.py +93 -0
- autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
- autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
- autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
- autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
- autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
- autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
- autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
- autobyteus/workflow/context/__init__.py +17 -0
- autobyteus/workflow/context/team_manager.py +147 -0
- autobyteus/workflow/context/workflow_config.py +30 -0
- autobyteus/workflow/context/workflow_context.py +61 -0
- autobyteus/workflow/context/workflow_node_config.py +76 -0
- autobyteus/workflow/context/workflow_runtime_state.py +53 -0
- autobyteus/workflow/events/__init__.py +29 -0
- autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
- autobyteus/workflow/events/workflow_events.py +53 -0
- autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
- autobyteus/workflow/exceptions.py +8 -0
- autobyteus/workflow/factory/__init__.py +9 -0
- autobyteus/workflow/factory/workflow_factory.py +99 -0
- autobyteus/workflow/handlers/__init__.py +19 -0
- autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
- autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
- autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
- autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
- autobyteus/workflow/phases/__init__.py +11 -0
- autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
- autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
- autobyteus/workflow/runtime/__init__.py +13 -0
- autobyteus/workflow/runtime/workflow_runtime.py +82 -0
- autobyteus/workflow/runtime/workflow_worker.py +117 -0
- autobyteus/workflow/shutdown_steps/__init__.py +17 -0
- autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
- autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
- autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
- autobyteus/workflow/streaming/__init__.py +26 -0
- autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
- autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
- autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
- autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
- autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
- autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
- autobyteus/workflow/utils/__init__.py +9 -0
- autobyteus/workflow/utils/wait_for_idle.py +46 -0
- autobyteus/workflow/workflow_builder.py +151 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/METADATA +16 -14
- autobyteus-1.1.5.dist-info/RECORD +455 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/top_level.txt +1 -0
- examples/__init__.py +1 -0
- examples/agent_team/__init__.py +1 -0
- examples/discover_phase_transitions.py +104 -0
- examples/run_browser_agent.py +262 -0
- examples/run_google_slides_agent.py +287 -0
- examples/run_mcp_browser_client.py +174 -0
- examples/run_mcp_google_slides_client.py +270 -0
- examples/run_mcp_list_tools.py +189 -0
- examples/run_poem_writer.py +284 -0
- examples/run_sqlite_agent.py +295 -0
- autobyteus/agent/context/agent_phase_manager.py +0 -264
- autobyteus/agent/context/phases.py +0 -49
- autobyteus/agent/group/__init__.py +0 -0
- autobyteus/agent/group/agent_group.py +0 -164
- autobyteus/agent/group/agent_group_context.py +0 -81
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
- autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
- autobyteus/agent/workflow/__init__.py +0 -11
- autobyteus/agent/workflow/agentic_workflow.py +0 -89
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
- autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
- autobyteus/tools/mcp/registrar.py +0 -202
- autobyteus/tools/usage/providers/json_example_provider.py +0 -32
- autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
- autobyteus/workflow/simple_task.py +0 -98
- autobyteus/workflow/task.py +0 -147
- autobyteus/workflow/workflow.py +0 -49
- autobyteus-1.1.3.dist-info/RECORD +0 -312
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/workflow/runtime/workflow_runtime.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Callable, Optional
|
|
5
|
+
|
|
6
|
+
from autobyteus.workflow.context.workflow_context import WorkflowContext
|
|
7
|
+
from autobyteus.workflow.phases.workflow_phase_manager import WorkflowPhaseManager
|
|
8
|
+
from autobyteus.workflow.runtime.workflow_worker import WorkflowWorker
|
|
9
|
+
from autobyteus.workflow.events.workflow_events import BaseWorkflowEvent
|
|
10
|
+
from autobyteus.workflow.streaming.workflow_event_notifier import WorkflowExternalEventNotifier
|
|
11
|
+
from autobyteus.workflow.streaming.agent_event_multiplexer import AgentEventMultiplexer
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from autobyteus.workflow.handlers.workflow_event_handler_registry import WorkflowEventHandlerRegistry
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class WorkflowRuntime:
|
|
19
|
+
"""The active execution engine for a workflow, managing the worker."""
|
|
20
|
+
def __init__(self, context: WorkflowContext, event_handler_registry: 'WorkflowEventHandlerRegistry'):
|
|
21
|
+
self.context = context
|
|
22
|
+
self.notifier = WorkflowExternalEventNotifier(workflow_id=self.context.workflow_id, runtime_ref=self)
|
|
23
|
+
self.phase_manager = WorkflowPhaseManager(context=self.context, notifier=self.notifier)
|
|
24
|
+
|
|
25
|
+
# --- FIX: Set the phase_manager_ref on the context's state BEFORE creating the worker ---
|
|
26
|
+
self.context.state.phase_manager_ref = self.phase_manager
|
|
27
|
+
|
|
28
|
+
self._worker = WorkflowWorker(self.context, event_handler_registry)
|
|
29
|
+
|
|
30
|
+
self.multiplexer = AgentEventMultiplexer(
|
|
31
|
+
workflow_id=self.context.workflow_id,
|
|
32
|
+
notifier=self.notifier,
|
|
33
|
+
worker_ref=self._worker
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Set other references on the context's state object for access by other components
|
|
37
|
+
self.context.state.multiplexer_ref = self.multiplexer
|
|
38
|
+
|
|
39
|
+
self._worker.add_done_callback(self._handle_worker_completion)
|
|
40
|
+
logger.info(f"WorkflowRuntime initialized for workflow '{self.context.workflow_id}'.")
|
|
41
|
+
|
|
42
|
+
def get_worker_loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
|
43
|
+
"""Returns the worker's event loop if it's running."""
|
|
44
|
+
return self._worker.get_worker_loop()
|
|
45
|
+
|
|
46
|
+
def _handle_worker_completion(self, future: asyncio.Future):
|
|
47
|
+
workflow_id = self.context.workflow_id
|
|
48
|
+
try:
|
|
49
|
+
future.result()
|
|
50
|
+
logger.info(f"WorkflowRuntime '{workflow_id}': Worker thread completed.")
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error(f"WorkflowRuntime '{workflow_id}': Worker thread terminated with exception: {e}", exc_info=True)
|
|
53
|
+
if not self.context.state.current_phase.is_terminal():
|
|
54
|
+
asyncio.run(self.phase_manager.notify_final_shutdown_complete())
|
|
55
|
+
|
|
56
|
+
def start(self):
|
|
57
|
+
if self._worker.is_alive:
|
|
58
|
+
return
|
|
59
|
+
self._worker.start()
|
|
60
|
+
|
|
61
|
+
async def stop(self, timeout: float = 10.0):
|
|
62
|
+
await self.phase_manager.notify_shutdown_initiated()
|
|
63
|
+
await self._worker.stop(timeout=timeout)
|
|
64
|
+
await self.phase_manager.notify_final_shutdown_complete()
|
|
65
|
+
|
|
66
|
+
async def submit_event(self, event: BaseWorkflowEvent):
|
|
67
|
+
if not self._worker.is_alive:
|
|
68
|
+
raise RuntimeError("Workflow worker is not active.")
|
|
69
|
+
def _coro_factory():
|
|
70
|
+
async def _enqueue():
|
|
71
|
+
from autobyteus.workflow.events.workflow_events import ProcessUserMessageEvent
|
|
72
|
+
if isinstance(event, ProcessUserMessageEvent):
|
|
73
|
+
await self.context.state.input_event_queues.enqueue_user_message(event)
|
|
74
|
+
else:
|
|
75
|
+
await self.context.state.input_event_queues.enqueue_internal_system_event(event)
|
|
76
|
+
return _enqueue()
|
|
77
|
+
future = self._worker.schedule_coroutine(_coro_factory)
|
|
78
|
+
await asyncio.wrap_future(future)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_running(self) -> bool:
|
|
82
|
+
return self._worker.is_alive
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/workflow/runtime/workflow_worker.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
from typing import TYPE_CHECKING, Optional, Callable, Awaitable, Any
|
|
6
|
+
|
|
7
|
+
from autobyteus.workflow.events.workflow_event_dispatcher import WorkflowEventDispatcher
|
|
8
|
+
from autobyteus.workflow.bootstrap_steps.workflow_bootstrapper import WorkflowBootstrapper
|
|
9
|
+
from autobyteus.workflow.shutdown_steps.workflow_shutdown_orchestrator import WorkflowShutdownOrchestrator
|
|
10
|
+
from autobyteus.agent.runtime.agent_thread_pool_manager import AgentThreadPoolManager
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from autobyteus.workflow.context.workflow_context import WorkflowContext
|
|
14
|
+
from autobyteus.workflow.handlers.workflow_event_handler_registry import WorkflowEventHandlerRegistry
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class WorkflowWorker:
|
|
19
|
+
"""Encapsulates the core event processing loop for a workflow."""
|
|
20
|
+
def __init__(self, context: 'WorkflowContext', event_handler_registry: 'WorkflowEventHandlerRegistry'):
|
|
21
|
+
self.context = context
|
|
22
|
+
self.phase_manager = self.context.phase_manager
|
|
23
|
+
self.event_dispatcher = WorkflowEventDispatcher(event_handler_registry, self.phase_manager)
|
|
24
|
+
|
|
25
|
+
self._thread_pool_manager = AgentThreadPoolManager()
|
|
26
|
+
self._thread_future: Optional[concurrent.futures.Future] = None
|
|
27
|
+
self._worker_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
28
|
+
self._async_stop_event: Optional[asyncio.Event] = None
|
|
29
|
+
self._is_active: bool = False
|
|
30
|
+
self._stop_initiated: bool = False
|
|
31
|
+
self._done_callbacks: list[Callable[[concurrent.futures.Future], None]] = []
|
|
32
|
+
logger.info(f"WorkflowWorker initialized for workflow '{self.context.workflow_id}'.")
|
|
33
|
+
|
|
34
|
+
def get_worker_loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
|
35
|
+
"""Returns the worker's event loop if it's running."""
|
|
36
|
+
return self._worker_loop if self._worker_loop and self._worker_loop.is_running() else None
|
|
37
|
+
|
|
38
|
+
def add_done_callback(self, callback: Callable):
|
|
39
|
+
if self._thread_future:
|
|
40
|
+
self._thread_future.add_done_callback(callback)
|
|
41
|
+
else:
|
|
42
|
+
self._done_callbacks.append(callback)
|
|
43
|
+
|
|
44
|
+
def schedule_coroutine(self, coro_factory: Callable[[], Awaitable[Any]]) -> concurrent.futures.Future:
|
|
45
|
+
if not self._worker_loop:
|
|
46
|
+
raise RuntimeError("WorkflowWorker loop is not available.")
|
|
47
|
+
return asyncio.run_coroutine_threadsafe(coro_factory(), self._worker_loop)
|
|
48
|
+
|
|
49
|
+
def start(self):
|
|
50
|
+
if self._is_active:
|
|
51
|
+
return
|
|
52
|
+
self._is_active = True
|
|
53
|
+
self._thread_future = self._thread_pool_manager.submit_task(self._run_managed_loop)
|
|
54
|
+
for cb in self._done_callbacks:
|
|
55
|
+
self._thread_future.add_done_callback(cb)
|
|
56
|
+
self._done_callbacks.clear()
|
|
57
|
+
|
|
58
|
+
def _run_managed_loop(self):
|
|
59
|
+
try:
|
|
60
|
+
self._worker_loop = asyncio.new_event_loop()
|
|
61
|
+
asyncio.set_event_loop(self._worker_loop)
|
|
62
|
+
self._async_stop_event = asyncio.Event()
|
|
63
|
+
self._worker_loop.run_until_complete(self.async_run())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"WorkflowWorker '{self.context.workflow_id}' event loop crashed: {e}", exc_info=True)
|
|
66
|
+
finally:
|
|
67
|
+
if self._worker_loop:
|
|
68
|
+
self._worker_loop.close()
|
|
69
|
+
self._is_active = False
|
|
70
|
+
|
|
71
|
+
async def async_run(self):
|
|
72
|
+
bootstrapper = WorkflowBootstrapper()
|
|
73
|
+
if not await bootstrapper.run(self.context, self.phase_manager):
|
|
74
|
+
logger.critical(f"Workflow '{self.context.workflow_id}' failed to initialize. Shutting down.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
logger.info(f"Workflow '{self.context.workflow_id}' entering main event loop.")
|
|
78
|
+
while not self._async_stop_event.is_set():
|
|
79
|
+
try:
|
|
80
|
+
# Combine queues for a single wait point
|
|
81
|
+
user_message_task = asyncio.create_task(self.context.state.input_event_queues.user_message_queue.get())
|
|
82
|
+
system_task = asyncio.create_task(self.context.state.input_event_queues.internal_system_event_queue.get())
|
|
83
|
+
done, pending = await asyncio.wait([user_message_task, system_task], return_when=asyncio.FIRST_COMPLETED, timeout=0.2)
|
|
84
|
+
|
|
85
|
+
for task in pending:
|
|
86
|
+
task.cancel()
|
|
87
|
+
|
|
88
|
+
if not done:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
event = done.pop().result()
|
|
92
|
+
await self.event_dispatcher.dispatch(event, self.context)
|
|
93
|
+
|
|
94
|
+
except asyncio.TimeoutError:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
logger.info(f"Workflow '{self.context.workflow_id}' shutdown signal received. Cleaning up.")
|
|
98
|
+
shutdown_orchestrator = WorkflowShutdownOrchestrator()
|
|
99
|
+
await shutdown_orchestrator.run(self.context)
|
|
100
|
+
|
|
101
|
+
async def stop(self, timeout: float = 10.0):
|
|
102
|
+
if not self._is_active or self._stop_initiated:
|
|
103
|
+
return
|
|
104
|
+
self._stop_initiated = True
|
|
105
|
+
if self._worker_loop:
|
|
106
|
+
self._worker_loop.call_soon_threadsafe(self._async_stop_event.set)
|
|
107
|
+
if self._thread_future:
|
|
108
|
+
try:
|
|
109
|
+
# FIX: Use asyncio.wait_for() to handle the timeout correctly.
|
|
110
|
+
await asyncio.wait_for(asyncio.wrap_future(self._thread_future), timeout=timeout)
|
|
111
|
+
except asyncio.TimeoutError:
|
|
112
|
+
logger.warning(f"Timeout waiting for workflow worker '{self.context.workflow_id}' to terminate.")
|
|
113
|
+
self._is_active = False
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_alive(self) -> bool:
|
|
117
|
+
return self._thread_future is not None and not self._thread_future.done()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/workflow/shutdown_steps/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Defines individual, self-contained steps for the workflow shutdown process.
|
|
4
|
+
"""
|
|
5
|
+
from autobyteus.workflow.shutdown_steps.base_workflow_shutdown_step import BaseWorkflowShutdownStep
|
|
6
|
+
from autobyteus.workflow.shutdown_steps.agent_team_shutdown_step import AgentTeamShutdownStep
|
|
7
|
+
from autobyteus.workflow.shutdown_steps.sub_workflow_shutdown_step import SubWorkflowShutdownStep
|
|
8
|
+
from autobyteus.workflow.shutdown_steps.bridge_cleanup_step import BridgeCleanupStep
|
|
9
|
+
from autobyteus.workflow.shutdown_steps.workflow_shutdown_orchestrator import WorkflowShutdownOrchestrator
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseWorkflowShutdownStep",
|
|
13
|
+
"AgentTeamShutdownStep",
|
|
14
|
+
"SubWorkflowShutdownStep",
|
|
15
|
+
"BridgeCleanupStep",
|
|
16
|
+
"WorkflowShutdownOrchestrator",
|
|
17
|
+
]
|
|
@@ -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)
|