autobyteus 1.0.5__py3-none-any.whl → 1.1.0__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 +97 -222
- autobyteus/agent/bootstrap_steps/__init__.py +19 -0
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +88 -0
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +57 -0
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +38 -0
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +93 -0
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +47 -0
- autobyteus/agent/context/__init__.py +18 -0
- autobyteus/agent/context/agent_config.py +80 -0
- autobyteus/agent/context/agent_context.py +132 -0
- autobyteus/agent/context/agent_phase_manager.py +164 -0
- autobyteus/agent/context/agent_runtime_state.py +89 -0
- autobyteus/agent/context/phases.py +47 -0
- autobyteus/agent/events/__init__.py +63 -0
- autobyteus/agent/events/agent_events.py +147 -0
- autobyteus/agent/events/agent_input_event_queue_manager.py +174 -0
- autobyteus/agent/events/notifiers.py +104 -0
- autobyteus/agent/events/worker_event_dispatcher.py +118 -0
- autobyteus/agent/factory/__init__.py +9 -0
- autobyteus/agent/factory/agent_factory.py +126 -79
- autobyteus/agent/group/agent_group.py +155 -0
- autobyteus/agent/group/agent_group_context.py +81 -0
- autobyteus/agent/handlers/__init__.py +36 -0
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +134 -0
- autobyteus/agent/handlers/base_event_handler.py +36 -0
- autobyteus/agent/handlers/event_handler_registry.py +76 -0
- autobyteus/agent/handlers/generic_event_handler.py +46 -0
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +76 -0
- autobyteus/agent/handlers/lifecycle_event_logger.py +64 -0
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +136 -0
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +140 -0
- autobyteus/agent/handlers/tool_execution_approval_event_handler.py +85 -0
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +186 -0
- autobyteus/agent/handlers/tool_result_event_handler.py +96 -0
- autobyteus/agent/handlers/user_input_message_event_handler.py +77 -0
- autobyteus/agent/hooks/__init__.py +9 -0
- autobyteus/agent/hooks/base_phase_hook.py +52 -0
- autobyteus/agent/input_processor/__init__.py +18 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +51 -0
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +41 -0
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +34 -0
- autobyteus/agent/input_processor/passthrough_input_processor.py +32 -0
- autobyteus/agent/input_processor/processor_definition.py +42 -0
- autobyteus/agent/input_processor/processor_meta.py +46 -0
- autobyteus/agent/input_processor/processor_registry.py +98 -0
- autobyteus/agent/llm_response_processor/__init__.py +16 -0
- autobyteus/agent/llm_response_processor/base_processor.py +50 -0
- autobyteus/agent/llm_response_processor/processor_definition.py +36 -0
- autobyteus/agent/llm_response_processor/processor_meta.py +37 -0
- autobyteus/agent/llm_response_processor/processor_registry.py +94 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +53 -0
- autobyteus/agent/message/__init__.py +20 -0
- autobyteus/agent/message/agent_input_user_message.py +96 -0
- autobyteus/agent/message/context_file.py +82 -0
- autobyteus/agent/message/context_file_type.py +64 -0
- autobyteus/agent/message/{message.py → inter_agent_message.py} +12 -12
- autobyteus/agent/message/{message_types.py → inter_agent_message_type.py} +8 -6
- autobyteus/agent/message/send_message_to.py +142 -36
- autobyteus/agent/remote_agent.py +240 -5
- autobyteus/agent/runtime/__init__.py +15 -0
- autobyteus/agent/runtime/agent_runtime.py +139 -0
- autobyteus/agent/runtime/agent_thread_pool_manager.py +88 -0
- autobyteus/agent/runtime/agent_worker.py +200 -0
- autobyteus/agent/streaming/__init__.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +120 -0
- autobyteus/agent/streaming/queue_streamer.py +58 -0
- autobyteus/agent/streaming/stream_event_payloads.py +156 -0
- autobyteus/agent/streaming/stream_events.py +123 -0
- autobyteus/agent/system_prompt_processor/__init__.py +14 -0
- autobyteus/agent/system_prompt_processor/base_processor.py +45 -0
- autobyteus/agent/system_prompt_processor/processor_definition.py +40 -0
- autobyteus/agent/system_prompt_processor/processor_meta.py +47 -0
- autobyteus/agent/system_prompt_processor/processor_registry.py +119 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +65 -0
- autobyteus/agent/tool_invocation.py +28 -5
- autobyteus/agent/utils/__init__.py +9 -0
- autobyteus/agent/utils/wait_for_idle.py +59 -0
- autobyteus/agent/workflow/__init__.py +11 -0
- autobyteus/agent/workflow/agentic_workflow.py +89 -0
- autobyteus/agent/workflow/base_agentic_workflow.py +98 -0
- autobyteus/agent/workspace/__init__.py +9 -0
- autobyteus/agent/workspace/base_workspace.py +55 -0
- autobyteus/cli/__init__.py +10 -0
- autobyteus/cli/agent_cli.py +299 -0
- autobyteus/events/event_emitter.py +33 -56
- autobyteus/events/event_manager.py +133 -66
- autobyteus/events/event_types.py +41 -14
- autobyteus/llm/api/autobyteus_llm.py +13 -15
- autobyteus/llm/api/bedrock_llm.py +9 -3
- autobyteus/llm/api/claude_llm.py +10 -5
- autobyteus/llm/api/deepseek_llm.py +53 -91
- autobyteus/llm/api/gemini_llm.py +10 -4
- autobyteus/llm/api/grok_llm.py +53 -77
- autobyteus/llm/api/groq_llm.py +10 -5
- autobyteus/llm/api/mistral_llm.py +10 -5
- autobyteus/llm/api/nvidia_llm.py +9 -4
- autobyteus/llm/api/ollama_llm.py +56 -48
- autobyteus/llm/api/openai_llm.py +20 -14
- autobyteus/llm/base_llm.py +95 -34
- autobyteus/llm/extensions/base_extension.py +3 -4
- autobyteus/llm/extensions/token_usage_tracking_extension.py +2 -3
- autobyteus/llm/llm_factory.py +12 -13
- autobyteus/llm/models.py +87 -8
- autobyteus/llm/user_message.py +73 -0
- autobyteus/llm/utils/llm_config.py +124 -27
- autobyteus/llm/utils/response_types.py +3 -2
- autobyteus/llm/utils/token_usage.py +7 -4
- autobyteus/rpc/__init__.py +73 -0
- autobyteus/rpc/client/__init__.py +17 -0
- autobyteus/rpc/client/abstract_client_connection.py +124 -0
- autobyteus/rpc/client/client_connection_manager.py +153 -0
- autobyteus/rpc/client/sse_client_connection.py +306 -0
- autobyteus/rpc/client/stdio_client_connection.py +280 -0
- autobyteus/rpc/config/__init__.py +13 -0
- autobyteus/rpc/config/agent_server_config.py +153 -0
- autobyteus/rpc/config/agent_server_registry.py +152 -0
- autobyteus/rpc/hosting.py +244 -0
- autobyteus/rpc/protocol.py +244 -0
- autobyteus/rpc/server/__init__.py +20 -0
- autobyteus/rpc/server/agent_server_endpoint.py +181 -0
- autobyteus/rpc/server/base_method_handler.py +40 -0
- autobyteus/rpc/server/method_handlers.py +259 -0
- autobyteus/rpc/server/sse_server_handler.py +182 -0
- autobyteus/rpc/server/stdio_server_handler.py +151 -0
- autobyteus/rpc/server_main.py +198 -0
- autobyteus/rpc/transport_type.py +13 -0
- autobyteus/tools/__init__.py +75 -0
- autobyteus/tools/ask_user_input.py +34 -77
- autobyteus/tools/base_tool.py +66 -37
- autobyteus/tools/bash/__init__.py +2 -0
- autobyteus/tools/bash/bash_executor.py +42 -79
- autobyteus/tools/browser/__init__.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +50 -42
- autobyteus/tools/browser/session_aware/browser_session_aware_tool.py +7 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +117 -125
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +75 -22
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +94 -28
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_web_element_trigger_factory.py +10 -2
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_reader_factory.py +18 -2
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_screenshot_taker_factory.py +10 -2
- autobyteus/tools/browser/session_aware/shared_browser_session_manager.py +4 -3
- autobyteus/tools/browser/standalone/__init__.py +7 -0
- autobyteus/tools/browser/standalone/factory/google_search_factory.py +17 -2
- autobyteus/tools/browser/standalone/factory/webpage_reader_factory.py +17 -2
- autobyteus/tools/browser/standalone/factory/webpage_screenshot_taker_factory.py +10 -2
- autobyteus/tools/browser/standalone/google_search_ui.py +104 -67
- autobyteus/tools/browser/standalone/navigate_to.py +52 -28
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +94 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +146 -61
- autobyteus/tools/browser/standalone/webpage_reader.py +80 -61
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +91 -45
- autobyteus/tools/factory/__init__.py +9 -0
- autobyteus/tools/factory/tool_factory.py +25 -4
- autobyteus/tools/file/file_reader.py +22 -51
- autobyteus/tools/file/file_writer.py +25 -56
- autobyteus/tools/functional_tool.py +234 -0
- autobyteus/tools/image_downloader.py +49 -71
- autobyteus/tools/mcp/__init__.py +47 -0
- autobyteus/tools/mcp/call_handlers/__init__.py +18 -0
- autobyteus/tools/mcp/call_handlers/base_handler.py +40 -0
- autobyteus/tools/mcp/call_handlers/sse_handler.py +22 -0
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +62 -0
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +55 -0
- autobyteus/tools/mcp/config_service.py +258 -0
- autobyteus/tools/mcp/factory.py +70 -0
- autobyteus/tools/mcp/registrar.py +135 -0
- autobyteus/tools/mcp/schema_mapper.py +131 -0
- autobyteus/tools/mcp/tool.py +101 -0
- autobyteus/tools/mcp/types.py +96 -0
- autobyteus/tools/parameter_schema.py +268 -0
- autobyteus/tools/pdf_downloader.py +78 -79
- autobyteus/tools/registry/__init__.py +0 -2
- autobyteus/tools/registry/tool_definition.py +106 -34
- autobyteus/tools/registry/tool_registry.py +46 -22
- autobyteus/tools/timer.py +150 -102
- autobyteus/tools/tool_config.py +117 -0
- autobyteus/tools/tool_meta.py +48 -26
- autobyteus/tools/usage/__init__.py +6 -0
- autobyteus/tools/usage/formatters/__init__.py +31 -0
- autobyteus/tools/usage/formatters/anthropic_json_example_formatter.py +18 -0
- autobyteus/tools/usage/formatters/anthropic_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/base_formatter.py +42 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +42 -0
- autobyteus/tools/usage/formatters/default_json_schema_formatter.py +28 -0
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +55 -0
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +46 -0
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +34 -0
- autobyteus/tools/usage/formatters/gemini_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +34 -0
- autobyteus/tools/usage/formatters/google_json_schema_formatter.py +25 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +49 -0
- autobyteus/tools/usage/formatters/openai_json_schema_formatter.py +28 -0
- autobyteus/tools/usage/parsers/__init__.py +22 -0
- autobyteus/tools/usage/parsers/anthropic_xml_tool_usage_parser.py +10 -0
- autobyteus/tools/usage/parsers/base_parser.py +41 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +106 -0
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +135 -0
- autobyteus/tools/usage/parsers/exceptions.py +13 -0
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +68 -0
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +147 -0
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +67 -0
- autobyteus/tools/usage/providers/__init__.py +22 -0
- autobyteus/tools/usage/providers/json_example_provider.py +32 -0
- autobyteus/tools/usage/providers/json_schema_provider.py +35 -0
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +28 -0
- autobyteus/tools/usage/providers/tool_manifest_provider.py +68 -0
- autobyteus/tools/usage/providers/xml_example_provider.py +28 -0
- autobyteus/tools/usage/providers/xml_schema_provider.py +29 -0
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +26 -0
- autobyteus/tools/usage/registries/__init__.py +20 -0
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +51 -0
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +51 -0
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +42 -0
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +30 -0
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +33 -0
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +30 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/METADATA +21 -3
- autobyteus-1.1.0.dist-info/RECORD +279 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/WHEEL +1 -1
- autobyteus/agent/async_agent.py +0 -175
- autobyteus/agent/async_group_aware_agent.py +0 -136
- autobyteus/agent/group/async_group_aware_agent.py +0 -122
- autobyteus/agent/group/coordinator_agent.py +0 -36
- autobyteus/agent/group/group_aware_agent.py +0 -121
- autobyteus/agent/orchestrator/__init__.py +0 -0
- autobyteus/agent/orchestrator/base_agent_orchestrator.py +0 -82
- autobyteus/agent/orchestrator/multi_replica_agent_orchestrator.py +0 -72
- autobyteus/agent/orchestrator/single_replica_agent_orchestrator.py +0 -43
- autobyteus/agent/registry/__init__.py +0 -11
- autobyteus/agent/registry/agent_definition.py +0 -94
- autobyteus/agent/registry/agent_registry.py +0 -114
- autobyteus/agent/response_parser/__init__.py +0 -0
- autobyteus/agent/response_parser/tool_usage_command_parser.py +0 -100
- autobyteus/agent/status.py +0 -12
- autobyteus/conversation/__init__.py +0 -0
- autobyteus/conversation/conversation.py +0 -54
- autobyteus/conversation/user_message.py +0 -59
- autobyteus/events/decorators.py +0 -29
- autobyteus/prompt/prompt_version_manager.py +0 -58
- autobyteus/prompt/storage/__init__.py +0 -0
- autobyteus/prompt/storage/prompt_version_model.py +0 -29
- autobyteus/prompt/storage/prompt_version_repository.py +0 -83
- autobyteus/tools/bash/factory/__init__.py +0 -0
- autobyteus/tools/bash/factory/bash_executor_factory.py +0 -6
- autobyteus/tools/factory/ask_user_input_factory.py +0 -6
- autobyteus/tools/factory/image_downloader_factory.py +0 -9
- autobyteus/tools/factory/pdf_downloader_factory.py +0 -9
- autobyteus/tools/factory/webpage_image_downloader_factory.py +0 -6
- autobyteus/tools/file/factory/__init__.py +0 -0
- autobyteus/tools/file/factory/file_reader_factory.py +0 -6
- autobyteus/tools/file/factory/file_writer_factory.py +0 -6
- autobyteus/tools/mcp_remote_tool.py +0 -82
- autobyteus/tools/web_page_pdf_generator.py +0 -90
- autobyteus-1.0.5.dist-info/RECORD +0 -163
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/runtime/agent_runtime.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import traceback
|
|
5
|
+
import concurrent.futures
|
|
6
|
+
from typing import Optional, Any, Callable, Awaitable, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from autobyteus.agent.context.agent_context import AgentContext
|
|
9
|
+
from autobyteus.agent.context.phases import AgentOperationalPhase
|
|
10
|
+
from autobyteus.agent.events.notifiers import AgentExternalEventNotifier
|
|
11
|
+
from autobyteus.agent.events import BaseEvent
|
|
12
|
+
from autobyteus.agent.context.agent_phase_manager import AgentPhaseManager
|
|
13
|
+
from autobyteus.agent.handlers import EventHandlerRegistry
|
|
14
|
+
from autobyteus.agent.runtime.agent_worker import AgentWorker
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
class AgentRuntime:
|
|
22
|
+
"""
|
|
23
|
+
The active execution engine for an agent. It creates and manages an AgentWorker.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self,
|
|
27
|
+
context: AgentContext,
|
|
28
|
+
event_handler_registry: EventHandlerRegistry):
|
|
29
|
+
|
|
30
|
+
self.context: AgentContext = context
|
|
31
|
+
self.event_handler_registry: EventHandlerRegistry = event_handler_registry
|
|
32
|
+
|
|
33
|
+
self.external_event_notifier: AgentExternalEventNotifier = AgentExternalEventNotifier(agent_id=self.context.agent_id)
|
|
34
|
+
self.phase_manager: AgentPhaseManager = AgentPhaseManager(context=self.context, notifier=self.external_event_notifier)
|
|
35
|
+
|
|
36
|
+
self.context.state.phase_manager_ref = self.phase_manager
|
|
37
|
+
|
|
38
|
+
self._worker: AgentWorker = AgentWorker(
|
|
39
|
+
context=self.context,
|
|
40
|
+
event_handler_registry=self.event_handler_registry,
|
|
41
|
+
)
|
|
42
|
+
self._worker.add_done_callback(self._handle_worker_completion)
|
|
43
|
+
|
|
44
|
+
logger.info(f"AgentRuntime initialized for agent_id '{self.context.agent_id}'.")
|
|
45
|
+
|
|
46
|
+
def get_worker_loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
|
47
|
+
return self._worker.get_worker_loop()
|
|
48
|
+
|
|
49
|
+
def _schedule_coroutine_on_worker(self, coro_factory: Callable[[], Awaitable[Any]]) -> concurrent.futures.Future:
|
|
50
|
+
worker_loop = self._worker.get_worker_loop()
|
|
51
|
+
if not worker_loop:
|
|
52
|
+
raise RuntimeError(f"AgentRuntime '{self.context.agent_id}': Worker loop not available.")
|
|
53
|
+
return self._worker.schedule_coroutine_on_worker_loop(coro_factory)
|
|
54
|
+
|
|
55
|
+
async def submit_event(self, event: BaseEvent) -> None:
|
|
56
|
+
from autobyteus.agent.events import UserMessageReceivedEvent, InterAgentMessageReceivedEvent, ToolExecutionApprovalEvent
|
|
57
|
+
|
|
58
|
+
agent_id = self.context.agent_id
|
|
59
|
+
if not self._worker or not self._worker.is_alive():
|
|
60
|
+
raise RuntimeError(f"Agent '{agent_id}' worker is not active.")
|
|
61
|
+
|
|
62
|
+
def _coro_factory() -> Awaitable[Any]:
|
|
63
|
+
async def _enqueue_coro():
|
|
64
|
+
if not self.context.state.input_event_queues:
|
|
65
|
+
logger.critical(f"AgentRuntime '{agent_id}': Input event queues not initialized for event {type(event).__name__}.")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if isinstance(event, UserMessageReceivedEvent):
|
|
69
|
+
await self.context.state.input_event_queues.enqueue_user_message(event)
|
|
70
|
+
elif isinstance(event, InterAgentMessageReceivedEvent):
|
|
71
|
+
await self.context.state.input_event_queues.enqueue_inter_agent_message(event)
|
|
72
|
+
elif isinstance(event, ToolExecutionApprovalEvent):
|
|
73
|
+
await self.context.state.input_event_queues.enqueue_tool_approval_event(event)
|
|
74
|
+
else:
|
|
75
|
+
await self.context.state.input_event_queues.enqueue_internal_system_event(event)
|
|
76
|
+
return _enqueue_coro()
|
|
77
|
+
|
|
78
|
+
future = self._schedule_coroutine_on_worker(_coro_factory)
|
|
79
|
+
await asyncio.wrap_future(future)
|
|
80
|
+
|
|
81
|
+
def start(self) -> None:
|
|
82
|
+
agent_id = self.context.agent_id
|
|
83
|
+
if self._worker.is_alive():
|
|
84
|
+
logger.warning(f"AgentRuntime for '{agent_id}' is already running. Ignoring start request.")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
logger.info(f"AgentRuntime for '{agent_id}': Starting worker.")
|
|
88
|
+
# Removed redundant phase notification. The first meaningful phase change to BOOTSTRAPPING
|
|
89
|
+
# is triggered by the AgentBootstrapper within the worker's async context.
|
|
90
|
+
# self.phase_manager.notify_runtime_starting_and_uninitialized()
|
|
91
|
+
self._worker.start()
|
|
92
|
+
logger.info(f"AgentRuntime for '{agent_id}': Worker start command issued. Worker will initialize itself.")
|
|
93
|
+
|
|
94
|
+
def _handle_worker_completion(self, future: concurrent.futures.Future):
|
|
95
|
+
agent_id = self.context.agent_id
|
|
96
|
+
try:
|
|
97
|
+
future.result()
|
|
98
|
+
logger.info(f"AgentRuntime '{agent_id}': Worker thread completed successfully.")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"AgentRuntime '{agent_id}': Worker thread terminated with an exception: {e}", exc_info=True)
|
|
101
|
+
if not self.context.current_phase.is_terminal():
|
|
102
|
+
# Since the phase manager is now async, we must run it in a new event loop.
|
|
103
|
+
try:
|
|
104
|
+
asyncio.run(self.phase_manager.notify_error_occurred("Worker thread exited unexpectedly.", traceback.format_exc()))
|
|
105
|
+
except Exception as run_e:
|
|
106
|
+
logger.critical(f"AgentRuntime '{agent_id}': Failed to run async error notification: {run_e}")
|
|
107
|
+
|
|
108
|
+
if not self.context.current_phase.is_terminal():
|
|
109
|
+
# Use asyncio.run() to execute the final async phase transition from a sync callback.
|
|
110
|
+
try:
|
|
111
|
+
asyncio.run(self.phase_manager.notify_final_shutdown_complete())
|
|
112
|
+
except Exception as run_e:
|
|
113
|
+
logger.critical(f"AgentRuntime '{agent_id}': Failed to run async final shutdown notification: {run_e}")
|
|
114
|
+
|
|
115
|
+
async def stop(self, timeout: float = 10.0) -> None:
|
|
116
|
+
agent_id = self.context.agent_id
|
|
117
|
+
if not self._worker.is_alive() and not self._worker._is_active:
|
|
118
|
+
if not self.context.current_phase.is_terminal():
|
|
119
|
+
await self.phase_manager.notify_final_shutdown_complete()
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
await self.phase_manager.notify_shutdown_initiated()
|
|
123
|
+
await self._worker.stop(timeout=timeout)
|
|
124
|
+
|
|
125
|
+
if self.context.llm_instance and hasattr(self.context.llm_instance, 'cleanup'):
|
|
126
|
+
cleanup_func = self.context.llm_instance.cleanup
|
|
127
|
+
if asyncio.iscoroutinefunction(cleanup_func): await cleanup_func()
|
|
128
|
+
else: cleanup_func()
|
|
129
|
+
|
|
130
|
+
await self.phase_manager.notify_final_shutdown_complete()
|
|
131
|
+
logger.info(f"AgentRuntime for '{agent_id}' stop() method completed.")
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def current_phase_property(self) -> AgentOperationalPhase:
|
|
135
|
+
return self.context.current_phase
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def is_running(self) -> bool:
|
|
139
|
+
return self._worker.is_alive()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/runtime/agent_thread_pool_manager.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import concurrent.futures
|
|
5
|
+
from typing import TYPE_CHECKING, Optional, Callable, Any
|
|
6
|
+
|
|
7
|
+
from autobyteus.utils.singleton import SingletonMeta
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class AgentThreadPoolManager(metaclass=SingletonMeta):
|
|
15
|
+
"""
|
|
16
|
+
A singleton manager for a shared ThreadPoolExecutor.
|
|
17
|
+
Used by agent components (like AgentWorker) to submit tasks
|
|
18
|
+
that need to run in a separate thread.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, max_workers: Optional[int] = None):
|
|
22
|
+
"""
|
|
23
|
+
Initializes the AgentThreadPoolManager's shared ThreadPoolExecutor.
|
|
24
|
+
This __init__ will be called only once by SingletonMeta upon first instantiation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
max_workers: The maximum number of threads in the pool.
|
|
28
|
+
Defaults to None (Python's default).
|
|
29
|
+
"""
|
|
30
|
+
self._thread_pool = concurrent.futures.ThreadPoolExecutor(
|
|
31
|
+
max_workers=max_workers,
|
|
32
|
+
thread_name_prefix="AgentThreadPool"
|
|
33
|
+
)
|
|
34
|
+
self._is_shutdown = False
|
|
35
|
+
logger.info(f"Singleton AgentThreadPoolManager initialized its ThreadPoolExecutor (max_workers={max_workers or 'default'}).")
|
|
36
|
+
|
|
37
|
+
def submit_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> concurrent.futures.Future:
|
|
38
|
+
"""
|
|
39
|
+
Submits a callable to be executed in the shared thread pool.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
func: The callable to execute.
|
|
43
|
+
*args: Positional arguments for the callable.
|
|
44
|
+
**kwargs: Keyword arguments for the callable.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A concurrent.futures.Future representing the execution of the callable.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RuntimeError: If the manager is shutdown.
|
|
51
|
+
"""
|
|
52
|
+
if self._is_shutdown: # pragma: no cover
|
|
53
|
+
raise RuntimeError("AgentThreadPoolManager is shutdown. Cannot submit new tasks.")
|
|
54
|
+
|
|
55
|
+
func_name = getattr(func, '__name__', repr(func))
|
|
56
|
+
logger.debug(f"AgentThreadPoolManager submitting task '{func_name}' to thread pool.")
|
|
57
|
+
|
|
58
|
+
future = self._thread_pool.submit(func, *args, **kwargs)
|
|
59
|
+
return future
|
|
60
|
+
|
|
61
|
+
def shutdown(self, wait: bool = True, cancel_futures: bool = False) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Shuts down the shared thread pool. This should typically be called at application exit.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
wait: If True, wait for all submitted tasks to complete.
|
|
67
|
+
cancel_futures: If True, attempt to cancel pending futures (Python 3.9+).
|
|
68
|
+
"""
|
|
69
|
+
if self._is_shutdown: # pragma: no cover
|
|
70
|
+
logger.info("AgentThreadPoolManager's shared ThreadPoolExecutor already requested to shutdown or is shutdown.")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
logger.info(f"AgentThreadPoolManager shutting down shared ThreadPoolExecutor (wait={wait}, cancel_futures={cancel_futures})...")
|
|
74
|
+
self._is_shutdown = True
|
|
75
|
+
|
|
76
|
+
import inspect
|
|
77
|
+
sig = inspect.signature(self._thread_pool.shutdown)
|
|
78
|
+
if 'cancel_futures' in sig.parameters:
|
|
79
|
+
self._thread_pool.shutdown(wait=wait, cancel_futures=cancel_futures)
|
|
80
|
+
else:
|
|
81
|
+
self._thread_pool.shutdown(wait=wait)
|
|
82
|
+
|
|
83
|
+
logger.info("AgentThreadPoolManager shared ThreadPoolExecutor shutdown process complete.")
|
|
84
|
+
|
|
85
|
+
def __del__(self): # pragma: no cover
|
|
86
|
+
if not self._is_shutdown:
|
|
87
|
+
logger.warning("AgentThreadPoolManager deleted without explicit shutdown. Attempting non-waiting shutdown of thread pool.")
|
|
88
|
+
self.shutdown(wait=False)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/runtime/agent_worker.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import traceback
|
|
5
|
+
import threading
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
from typing import TYPE_CHECKING, Optional, Any, Callable, Awaitable, List
|
|
8
|
+
|
|
9
|
+
from autobyteus.agent.context.phases import AgentOperationalPhase
|
|
10
|
+
from autobyteus.agent.events import (
|
|
11
|
+
BaseEvent,
|
|
12
|
+
AgentErrorEvent,
|
|
13
|
+
AgentStoppedEvent,
|
|
14
|
+
)
|
|
15
|
+
from autobyteus.agent.events import WorkerEventDispatcher
|
|
16
|
+
from autobyteus.agent.runtime.agent_thread_pool_manager import AgentThreadPoolManager
|
|
17
|
+
from autobyteus.agent.bootstrap_steps.agent_bootstrapper import AgentBootstrapper
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from autobyteus.agent.context import AgentContext
|
|
21
|
+
from autobyteus.agent.handlers import EventHandlerRegistry
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
class AgentWorker:
|
|
26
|
+
"""
|
|
27
|
+
Encapsulates the core event processing loop for an agent.
|
|
28
|
+
It manages its own execution in a dedicated thread, runs its own asyncio event loop,
|
|
29
|
+
and performs its own initialization sequence before processing external events.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self,
|
|
33
|
+
context: 'AgentContext',
|
|
34
|
+
event_handler_registry: 'EventHandlerRegistry'):
|
|
35
|
+
self.context: 'AgentContext' = context
|
|
36
|
+
|
|
37
|
+
self.phase_manager = self.context.phase_manager
|
|
38
|
+
if not self.phase_manager: # pragma: no cover
|
|
39
|
+
raise ValueError(f"AgentWorker for '{self.context.agent_id}': AgentPhaseManager not found.")
|
|
40
|
+
|
|
41
|
+
self.worker_event_dispatcher = WorkerEventDispatcher(
|
|
42
|
+
event_handler_registry=event_handler_registry,
|
|
43
|
+
phase_manager=self.phase_manager
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self._thread_pool_manager = AgentThreadPoolManager()
|
|
47
|
+
self._thread_future: Optional[concurrent.futures.Future] = None
|
|
48
|
+
self._worker_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
49
|
+
self._async_stop_event: Optional[asyncio.Event] = None
|
|
50
|
+
|
|
51
|
+
self._is_active: bool = False
|
|
52
|
+
self._stop_initiated: bool = False
|
|
53
|
+
|
|
54
|
+
self._done_callbacks: list[Callable[[concurrent.futures.Future], None]] = []
|
|
55
|
+
|
|
56
|
+
logger.info(f"AgentWorker initialized for agent_id '{self.context.agent_id}'.")
|
|
57
|
+
|
|
58
|
+
async def _initialize(self) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Runs the agent's initialization sequence by using an AgentBootstrapper.
|
|
61
|
+
Returns True on success, False on failure.
|
|
62
|
+
"""
|
|
63
|
+
agent_id = self.context.agent_id
|
|
64
|
+
logger.info(f"Agent '{agent_id}': Starting internal initialization process using AgentBootstrapper.")
|
|
65
|
+
|
|
66
|
+
bootstrapper = AgentBootstrapper() # Using default steps
|
|
67
|
+
initialization_successful = await bootstrapper.run(self.context, self.phase_manager)
|
|
68
|
+
|
|
69
|
+
return initialization_successful
|
|
70
|
+
|
|
71
|
+
def add_done_callback(self, callback: Callable[[concurrent.futures.Future], None]):
|
|
72
|
+
"""Adds a callback to be executed when the worker's thread completes."""
|
|
73
|
+
if self._thread_future:
|
|
74
|
+
self._thread_future.add_done_callback(callback)
|
|
75
|
+
else:
|
|
76
|
+
self._done_callbacks.append(callback)
|
|
77
|
+
|
|
78
|
+
def get_worker_loop(self) -> Optional[asyncio.AbstractEventLoop]:
|
|
79
|
+
"""Returns the worker's event loop if it's running."""
|
|
80
|
+
return self._worker_loop if self._worker_loop and self._worker_loop.is_running() else None
|
|
81
|
+
|
|
82
|
+
def schedule_coroutine_on_worker_loop(self, coro_factory: Callable[[], Awaitable[Any]]) -> concurrent.futures.Future:
|
|
83
|
+
"""Schedules a coroutine to be run on the worker's event loop from other threads."""
|
|
84
|
+
worker_loop = self.get_worker_loop()
|
|
85
|
+
if not worker_loop:
|
|
86
|
+
raise RuntimeError(f"AgentWorker '{self.context.agent_id}': Worker event loop is not available.")
|
|
87
|
+
return asyncio.run_coroutine_threadsafe(coro_factory(), worker_loop)
|
|
88
|
+
|
|
89
|
+
def start(self) -> None:
|
|
90
|
+
agent_id = self.context.agent_id
|
|
91
|
+
if self._is_active or (self._thread_future and not self._thread_future.done()):
|
|
92
|
+
logger.warning(f"AgentWorker '{agent_id}': Start called, but worker is already active or starting.")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
logger.info(f"AgentWorker '{agent_id}': Starting...")
|
|
96
|
+
self._is_active = True
|
|
97
|
+
self._stop_initiated = False
|
|
98
|
+
self._thread_future = self._thread_pool_manager.submit_task(self._run_managed_thread_loop)
|
|
99
|
+
for cb in self._done_callbacks:
|
|
100
|
+
self._thread_future.add_done_callback(cb)
|
|
101
|
+
self._done_callbacks.clear()
|
|
102
|
+
|
|
103
|
+
def _run_managed_thread_loop(self) -> None:
|
|
104
|
+
thread_name = threading.current_thread().name
|
|
105
|
+
agent_id = self.context.agent_id
|
|
106
|
+
logger.info(f"AgentWorker '{agent_id}': Thread '{thread_name}' started. Setting up asyncio event loop.")
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
self._worker_loop = asyncio.new_event_loop()
|
|
110
|
+
asyncio.set_event_loop(self._worker_loop)
|
|
111
|
+
self._async_stop_event = asyncio.Event()
|
|
112
|
+
self._worker_loop.run_until_complete(self.async_run())
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"AgentWorker '{agent_id}': Unhandled exception in _run_managed_thread_loop: {e}", exc_info=True)
|
|
115
|
+
if self.phase_manager and not self.context.current_phase.is_terminal():
|
|
116
|
+
try:
|
|
117
|
+
# Since this is a sync context, we must run the async phase manager method in a temporary event loop.
|
|
118
|
+
asyncio.run(self.phase_manager.notify_error_occurred(f"Worker thread fatal error: {e}", traceback.format_exc()))
|
|
119
|
+
except Exception as run_e:
|
|
120
|
+
logger.critical(f"AgentWorker '{agent_id}': Failed to run async error notification from sync context: {run_e}")
|
|
121
|
+
finally:
|
|
122
|
+
if self._worker_loop:
|
|
123
|
+
try:
|
|
124
|
+
# Gather all remaining tasks and cancel them
|
|
125
|
+
tasks = asyncio.all_tasks(loop=self._worker_loop)
|
|
126
|
+
for task in tasks:
|
|
127
|
+
task.cancel()
|
|
128
|
+
|
|
129
|
+
# Wait for all tasks to be cancelled
|
|
130
|
+
if tasks:
|
|
131
|
+
self._worker_loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
|
132
|
+
|
|
133
|
+
# Shutdown async generators
|
|
134
|
+
self._worker_loop.run_until_complete(self._worker_loop.shutdown_asyncgens())
|
|
135
|
+
except Exception as cleanup_exc: # pragma: no cover
|
|
136
|
+
logger.error(f"AgentWorker '{agent_id}': Exception during event loop cleanup: {cleanup_exc}", exc_info=True)
|
|
137
|
+
finally:
|
|
138
|
+
self._worker_loop.close()
|
|
139
|
+
self._is_active = False
|
|
140
|
+
|
|
141
|
+
async def async_run(self) -> None:
|
|
142
|
+
agent_id = self.context.agent_id
|
|
143
|
+
logger.info(f"AgentWorker '{agent_id}' async_run(): Starting.")
|
|
144
|
+
|
|
145
|
+
# --- Direct Initialization ---
|
|
146
|
+
initialization_successful = await self._initialize()
|
|
147
|
+
if not initialization_successful:
|
|
148
|
+
logger.critical(f"AgentWorker '{agent_id}' failed to initialize. Worker is shutting down.")
|
|
149
|
+
if self._async_stop_event and not self._async_stop_event.is_set():
|
|
150
|
+
self._async_stop_event.set()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# --- Main Event Loop ---
|
|
154
|
+
logger.info(f"AgentWorker '{agent_id}' initialized successfully. Entering main event loop.")
|
|
155
|
+
try:
|
|
156
|
+
while not self._async_stop_event.is_set():
|
|
157
|
+
try:
|
|
158
|
+
queue_event_tuple = await asyncio.wait_for(
|
|
159
|
+
self.context.state.input_event_queues.get_next_input_event(), timeout=0.1
|
|
160
|
+
)
|
|
161
|
+
except asyncio.TimeoutError:
|
|
162
|
+
if self._async_stop_event.is_set(): break
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
if queue_event_tuple is None:
|
|
166
|
+
if self._async_stop_event.is_set(): break
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
_queue_name, event_obj = queue_event_tuple
|
|
170
|
+
await self.worker_event_dispatcher.dispatch(event_obj, self.context)
|
|
171
|
+
await asyncio.sleep(0)
|
|
172
|
+
|
|
173
|
+
except asyncio.CancelledError:
|
|
174
|
+
logger.info(f"AgentWorker '{agent_id}' async_run() loop task was cancelled.")
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Fatal error in AgentWorker '{agent_id}' async_run() loop: {e}", exc_info=True)
|
|
177
|
+
finally:
|
|
178
|
+
logger.info(f"AgentWorker '{agent_id}' async_run() loop has finished.")
|
|
179
|
+
|
|
180
|
+
async def _signal_internal_stop(self):
|
|
181
|
+
if self._async_stop_event and not self._async_stop_event.is_set():
|
|
182
|
+
self._async_stop_event.set()
|
|
183
|
+
if self.context.state.input_event_queues:
|
|
184
|
+
await self.context.state.input_event_queues.enqueue_internal_system_event(AgentStoppedEvent())
|
|
185
|
+
|
|
186
|
+
async def stop(self, timeout: float = 10.0) -> None:
|
|
187
|
+
if not self._is_active or self._stop_initiated:
|
|
188
|
+
return
|
|
189
|
+
self._stop_initiated = True
|
|
190
|
+
if self.get_worker_loop() and self._async_stop_event:
|
|
191
|
+
future = asyncio.run_coroutine_threadsafe(self._signal_internal_stop(), self.get_worker_loop())
|
|
192
|
+
try: future.result(timeout=1.0)
|
|
193
|
+
except Exception: pass
|
|
194
|
+
if self._thread_future:
|
|
195
|
+
try: await asyncio.wait_for(asyncio.wrap_future(self._thread_future), timeout=timeout)
|
|
196
|
+
except asyncio.TimeoutError: logger.warning(f"Timeout waiting for worker thread of '{self.context.agent_id}'.")
|
|
197
|
+
self._is_active = False
|
|
198
|
+
|
|
199
|
+
def is_alive(self) -> bool:
|
|
200
|
+
return self._thread_future is not None and not self._thread_future.done()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/streaming/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Components related to agent output streaming, including event models, stream consumers, and streamer utilities.
|
|
4
|
+
"""
|
|
5
|
+
from .stream_events import StreamEventType, StreamEvent
|
|
6
|
+
from .agent_event_stream import AgentEventStream
|
|
7
|
+
from .queue_streamer import stream_queue_items
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"StreamEventType",
|
|
11
|
+
"StreamEvent",
|
|
12
|
+
"AgentEventStream",
|
|
13
|
+
"stream_queue_items",
|
|
14
|
+
]
|
|
15
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import traceback
|
|
4
|
+
import functools
|
|
5
|
+
import queue as standard_queue
|
|
6
|
+
from typing import AsyncIterator, Dict, Any, TYPE_CHECKING, List, Optional, Callable, Union
|
|
7
|
+
|
|
8
|
+
from autobyteus.llm.utils.response_types import ChunkResponse, CompleteResponse
|
|
9
|
+
from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
|
|
10
|
+
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
11
|
+
create_assistant_chunk_data,
|
|
12
|
+
create_assistant_complete_response_data,
|
|
13
|
+
create_tool_interaction_log_entry_data,
|
|
14
|
+
create_agent_operational_phase_transition_data,
|
|
15
|
+
create_error_event_data,
|
|
16
|
+
create_tool_invocation_approval_requested_data,
|
|
17
|
+
EmptyData,
|
|
18
|
+
StreamDataPayload,
|
|
19
|
+
ErrorEventData,
|
|
20
|
+
)
|
|
21
|
+
from .queue_streamer import stream_queue_items
|
|
22
|
+
from autobyteus.events.event_types import EventType
|
|
23
|
+
from autobyteus.events.event_emitter import EventEmitter
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from autobyteus.agent.agent import Agent
|
|
27
|
+
from autobyteus.agent.events.notifiers import AgentExternalEventNotifier
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
_AES_INTERNAL_SENTINEL = object()
|
|
32
|
+
|
|
33
|
+
class AgentEventStream(EventEmitter):
|
|
34
|
+
def __init__(self, agent: 'Agent'):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
from autobyteus.agent.agent import Agent as ConcreteAgent
|
|
38
|
+
if not isinstance(agent, ConcreteAgent):
|
|
39
|
+
raise TypeError(f"AgentEventStream requires an Agent instance, got {type(agent).__name__}.")
|
|
40
|
+
|
|
41
|
+
self.agent_id: str = agent.agent_id
|
|
42
|
+
|
|
43
|
+
self._loop = asyncio.get_event_loop()
|
|
44
|
+
self._generic_stream_event_internal_q: standard_queue.Queue[Union[StreamEvent, object]] = standard_queue.Queue()
|
|
45
|
+
|
|
46
|
+
self._notifier: Optional['AgentExternalEventNotifier'] = None
|
|
47
|
+
if agent.context and agent.context.phase_manager:
|
|
48
|
+
self._notifier = agent.context.phase_manager.notifier
|
|
49
|
+
|
|
50
|
+
if not self._notifier:
|
|
51
|
+
logger.error(f"AgentEventStream for '{self.agent_id}': Notifier not available. No events will be streamed.")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
self._register_listeners()
|
|
55
|
+
|
|
56
|
+
logger.info(f"AgentEventStream (ID: {self.object_id}) initialized for agent_id '{self.agent_id}'. Subscribed to notifier.")
|
|
57
|
+
|
|
58
|
+
def _register_listeners(self):
|
|
59
|
+
"""Subscribes this instance's handler to all relevant events from the notifier."""
|
|
60
|
+
all_agent_event_types = [et for et in EventType if et.name.startswith("AGENT_")]
|
|
61
|
+
|
|
62
|
+
for event_type in all_agent_event_types:
|
|
63
|
+
handler = functools.partial(self._handle_notifier_event_sync, event_type=event_type)
|
|
64
|
+
self.subscribe_from(self._notifier, event_type, handler)
|
|
65
|
+
|
|
66
|
+
def _handle_notifier_event_sync(self, event_type: EventType, payload: Optional[Any] = None, object_id: Optional[str] = None, **kwargs):
|
|
67
|
+
event_agent_id = kwargs.get("agent_id", self.agent_id)
|
|
68
|
+
|
|
69
|
+
typed_payload_for_stream_event: Optional[StreamDataPayload] = None
|
|
70
|
+
stream_event_type_for_generic_stream: Optional[StreamEventType] = None
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
if event_type == EventType.AGENT_PHASE_IDLE_ENTERED:
|
|
74
|
+
typed_payload_for_stream_event = create_agent_operational_phase_transition_data(payload)
|
|
75
|
+
stream_event_type_for_generic_stream = StreamEventType.AGENT_IDLE
|
|
76
|
+
elif event_type.name.startswith("AGENT_PHASE_"):
|
|
77
|
+
typed_payload_for_stream_event = create_agent_operational_phase_transition_data(payload)
|
|
78
|
+
stream_event_type_for_generic_stream = StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION
|
|
79
|
+
elif event_type == EventType.AGENT_DATA_ASSISTANT_CHUNK:
|
|
80
|
+
typed_payload_for_stream_event = create_assistant_chunk_data(payload)
|
|
81
|
+
stream_event_type_for_generic_stream = StreamEventType.ASSISTANT_CHUNK
|
|
82
|
+
elif event_type == EventType.AGENT_DATA_ASSISTANT_COMPLETE_RESPONSE:
|
|
83
|
+
typed_payload_for_stream_event = create_assistant_complete_response_data(payload)
|
|
84
|
+
stream_event_type_for_generic_stream = StreamEventType.ASSISTANT_COMPLETE_RESPONSE
|
|
85
|
+
elif event_type == EventType.AGENT_DATA_TOOL_LOG:
|
|
86
|
+
typed_payload_for_stream_event = create_tool_interaction_log_entry_data(payload)
|
|
87
|
+
stream_event_type_for_generic_stream = StreamEventType.TOOL_INTERACTION_LOG_ENTRY
|
|
88
|
+
elif event_type == EventType.AGENT_REQUEST_TOOL_INVOCATION_APPROVAL:
|
|
89
|
+
typed_payload_for_stream_event = create_tool_invocation_approval_requested_data(payload)
|
|
90
|
+
stream_event_type_for_generic_stream = StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED
|
|
91
|
+
elif event_type == EventType.AGENT_ERROR_OUTPUT_GENERATION:
|
|
92
|
+
typed_payload_for_stream_event = create_error_event_data(payload)
|
|
93
|
+
stream_event_type_for_generic_stream = StreamEventType.ERROR_EVENT
|
|
94
|
+
|
|
95
|
+
# The other queues are no longer needed, as `all_events` is the single source of truth.
|
|
96
|
+
elif event_type in [EventType.AGENT_DATA_ASSISTANT_CHUNK_STREAM_END, EventType.AGENT_DATA_TOOL_LOG_STREAM_END]:
|
|
97
|
+
pass # These events are signals, not data for the unified stream.
|
|
98
|
+
else:
|
|
99
|
+
logger.debug(f"AgentEventStream received internal event '{event_type.name}' with no direct stream mapping.")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"AgentEventStream error processing payload for event '{event_type.name}': {e}", exc_info=True)
|
|
103
|
+
|
|
104
|
+
if typed_payload_for_stream_event and stream_event_type_for_generic_stream:
|
|
105
|
+
stream_event = StreamEvent(
|
|
106
|
+
agent_id=event_agent_id,
|
|
107
|
+
event_type=stream_event_type_for_generic_stream,
|
|
108
|
+
data=typed_payload_for_stream_event
|
|
109
|
+
)
|
|
110
|
+
self._generic_stream_event_internal_q.put(stream_event)
|
|
111
|
+
|
|
112
|
+
async def close(self):
|
|
113
|
+
logger.info(f"AgentEventStream (ID: {self.object_id}) for '{self.agent_id}': close() called. Unsubscribing all listeners and signaling.")
|
|
114
|
+
self.unsubscribe_all_listeners()
|
|
115
|
+
await self._loop.run_in_executor(None, self._generic_stream_event_internal_q.put, _AES_INTERNAL_SENTINEL)
|
|
116
|
+
|
|
117
|
+
async def all_events(self) -> AsyncIterator[StreamEvent]:
|
|
118
|
+
"""The primary method to consume all structured events from the agent."""
|
|
119
|
+
async for event in stream_queue_items(self._generic_stream_event_internal_q, _AES_INTERNAL_SENTINEL, f"agent_{self.agent_id}_all_events"):
|
|
120
|
+
yield event
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/streaming/queue_streamer.py
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TypeVar, AsyncIterator, Union, Any
|
|
5
|
+
import queue as standard_queue
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
T = TypeVar('T')
|
|
10
|
+
|
|
11
|
+
async def stream_queue_items(
|
|
12
|
+
queue: standard_queue.Queue[Union[T, object]],
|
|
13
|
+
sentinel: object,
|
|
14
|
+
source_name: str = "unspecified_queue"
|
|
15
|
+
) -> AsyncIterator[T]:
|
|
16
|
+
"""
|
|
17
|
+
Asynchronously iterates over a standard `queue.Queue`, yielding items of type T
|
|
18
|
+
until a specific sentinel object is encountered. This is designed to be used
|
|
19
|
+
from an async context to consume from a queue populated by a synchronous/threaded context.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
queue: The standard `queue.Queue` to stream items from.
|
|
23
|
+
sentinel: The unique object used to signal the end of data in the queue.
|
|
24
|
+
source_name: An optional identifier for the queue source, used in logging.
|
|
25
|
+
|
|
26
|
+
Yields:
|
|
27
|
+
Items of type T from the queue.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
TypeError: If queue is not a `queue.Queue`.
|
|
31
|
+
ValueError: If sentinel is None.
|
|
32
|
+
asyncio.CancelledError: If the generator is cancelled.
|
|
33
|
+
Exception: Propagates exceptions encountered during queue.get().
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(queue, standard_queue.Queue):
|
|
36
|
+
raise TypeError(f"queue must be an instance of queue.Queue for source '{source_name}'.")
|
|
37
|
+
if sentinel is None:
|
|
38
|
+
raise ValueError(f"sentinel object cannot be None for source '{source_name}'.")
|
|
39
|
+
|
|
40
|
+
logger.debug(f"Starting to stream items from queue '{source_name}'.")
|
|
41
|
+
loop = asyncio.get_running_loop()
|
|
42
|
+
try:
|
|
43
|
+
while True:
|
|
44
|
+
# Use run_in_executor to wait for the blocking get() call without blocking the event loop.
|
|
45
|
+
item: Any = await loop.run_in_executor(None, queue.get)
|
|
46
|
+
if item is sentinel:
|
|
47
|
+
logger.debug(f"Sentinel {sentinel!r} received from queue '{source_name}'. Ending stream.")
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
yield item # type: ignore
|
|
51
|
+
except asyncio.CancelledError:
|
|
52
|
+
logger.info(f"Stream from queue '{source_name}' was cancelled.")
|
|
53
|
+
raise
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Error streaming from queue '{source_name}': {e}", exc_info=True)
|
|
56
|
+
raise
|
|
57
|
+
finally:
|
|
58
|
+
logger.debug(f"Exiting stream_queue_items for queue '{source_name}'.")
|