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.
Files changed (256) hide show
  1. autobyteus/agent/agent.py +97 -222
  2. autobyteus/agent/bootstrap_steps/__init__.py +19 -0
  3. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +88 -0
  4. autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +57 -0
  5. autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +38 -0
  6. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +93 -0
  7. autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +47 -0
  8. autobyteus/agent/context/__init__.py +18 -0
  9. autobyteus/agent/context/agent_config.py +80 -0
  10. autobyteus/agent/context/agent_context.py +132 -0
  11. autobyteus/agent/context/agent_phase_manager.py +164 -0
  12. autobyteus/agent/context/agent_runtime_state.py +89 -0
  13. autobyteus/agent/context/phases.py +47 -0
  14. autobyteus/agent/events/__init__.py +63 -0
  15. autobyteus/agent/events/agent_events.py +147 -0
  16. autobyteus/agent/events/agent_input_event_queue_manager.py +174 -0
  17. autobyteus/agent/events/notifiers.py +104 -0
  18. autobyteus/agent/events/worker_event_dispatcher.py +118 -0
  19. autobyteus/agent/factory/__init__.py +9 -0
  20. autobyteus/agent/factory/agent_factory.py +126 -79
  21. autobyteus/agent/group/agent_group.py +155 -0
  22. autobyteus/agent/group/agent_group_context.py +81 -0
  23. autobyteus/agent/handlers/__init__.py +36 -0
  24. autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +134 -0
  25. autobyteus/agent/handlers/base_event_handler.py +36 -0
  26. autobyteus/agent/handlers/event_handler_registry.py +76 -0
  27. autobyteus/agent/handlers/generic_event_handler.py +46 -0
  28. autobyteus/agent/handlers/inter_agent_message_event_handler.py +76 -0
  29. autobyteus/agent/handlers/lifecycle_event_logger.py +64 -0
  30. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +136 -0
  31. autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +140 -0
  32. autobyteus/agent/handlers/tool_execution_approval_event_handler.py +85 -0
  33. autobyteus/agent/handlers/tool_invocation_request_event_handler.py +186 -0
  34. autobyteus/agent/handlers/tool_result_event_handler.py +96 -0
  35. autobyteus/agent/handlers/user_input_message_event_handler.py +77 -0
  36. autobyteus/agent/hooks/__init__.py +9 -0
  37. autobyteus/agent/hooks/base_phase_hook.py +52 -0
  38. autobyteus/agent/input_processor/__init__.py +18 -0
  39. autobyteus/agent/input_processor/base_user_input_processor.py +51 -0
  40. autobyteus/agent/input_processor/content_prefixing_input_processor.py +41 -0
  41. autobyteus/agent/input_processor/metadata_appending_input_processor.py +34 -0
  42. autobyteus/agent/input_processor/passthrough_input_processor.py +32 -0
  43. autobyteus/agent/input_processor/processor_definition.py +42 -0
  44. autobyteus/agent/input_processor/processor_meta.py +46 -0
  45. autobyteus/agent/input_processor/processor_registry.py +98 -0
  46. autobyteus/agent/llm_response_processor/__init__.py +16 -0
  47. autobyteus/agent/llm_response_processor/base_processor.py +50 -0
  48. autobyteus/agent/llm_response_processor/processor_definition.py +36 -0
  49. autobyteus/agent/llm_response_processor/processor_meta.py +37 -0
  50. autobyteus/agent/llm_response_processor/processor_registry.py +94 -0
  51. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +53 -0
  52. autobyteus/agent/message/__init__.py +20 -0
  53. autobyteus/agent/message/agent_input_user_message.py +96 -0
  54. autobyteus/agent/message/context_file.py +82 -0
  55. autobyteus/agent/message/context_file_type.py +64 -0
  56. autobyteus/agent/message/{message.py → inter_agent_message.py} +12 -12
  57. autobyteus/agent/message/{message_types.py → inter_agent_message_type.py} +8 -6
  58. autobyteus/agent/message/send_message_to.py +142 -36
  59. autobyteus/agent/remote_agent.py +240 -5
  60. autobyteus/agent/runtime/__init__.py +15 -0
  61. autobyteus/agent/runtime/agent_runtime.py +139 -0
  62. autobyteus/agent/runtime/agent_thread_pool_manager.py +88 -0
  63. autobyteus/agent/runtime/agent_worker.py +200 -0
  64. autobyteus/agent/streaming/__init__.py +15 -0
  65. autobyteus/agent/streaming/agent_event_stream.py +120 -0
  66. autobyteus/agent/streaming/queue_streamer.py +58 -0
  67. autobyteus/agent/streaming/stream_event_payloads.py +156 -0
  68. autobyteus/agent/streaming/stream_events.py +123 -0
  69. autobyteus/agent/system_prompt_processor/__init__.py +14 -0
  70. autobyteus/agent/system_prompt_processor/base_processor.py +45 -0
  71. autobyteus/agent/system_prompt_processor/processor_definition.py +40 -0
  72. autobyteus/agent/system_prompt_processor/processor_meta.py +47 -0
  73. autobyteus/agent/system_prompt_processor/processor_registry.py +119 -0
  74. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +65 -0
  75. autobyteus/agent/tool_invocation.py +28 -5
  76. autobyteus/agent/utils/__init__.py +9 -0
  77. autobyteus/agent/utils/wait_for_idle.py +59 -0
  78. autobyteus/agent/workflow/__init__.py +11 -0
  79. autobyteus/agent/workflow/agentic_workflow.py +89 -0
  80. autobyteus/agent/workflow/base_agentic_workflow.py +98 -0
  81. autobyteus/agent/workspace/__init__.py +9 -0
  82. autobyteus/agent/workspace/base_workspace.py +55 -0
  83. autobyteus/cli/__init__.py +10 -0
  84. autobyteus/cli/agent_cli.py +299 -0
  85. autobyteus/events/event_emitter.py +33 -56
  86. autobyteus/events/event_manager.py +133 -66
  87. autobyteus/events/event_types.py +41 -14
  88. autobyteus/llm/api/autobyteus_llm.py +13 -15
  89. autobyteus/llm/api/bedrock_llm.py +9 -3
  90. autobyteus/llm/api/claude_llm.py +10 -5
  91. autobyteus/llm/api/deepseek_llm.py +53 -91
  92. autobyteus/llm/api/gemini_llm.py +10 -4
  93. autobyteus/llm/api/grok_llm.py +53 -77
  94. autobyteus/llm/api/groq_llm.py +10 -5
  95. autobyteus/llm/api/mistral_llm.py +10 -5
  96. autobyteus/llm/api/nvidia_llm.py +9 -4
  97. autobyteus/llm/api/ollama_llm.py +56 -48
  98. autobyteus/llm/api/openai_llm.py +20 -14
  99. autobyteus/llm/base_llm.py +95 -34
  100. autobyteus/llm/extensions/base_extension.py +3 -4
  101. autobyteus/llm/extensions/token_usage_tracking_extension.py +2 -3
  102. autobyteus/llm/llm_factory.py +12 -13
  103. autobyteus/llm/models.py +87 -8
  104. autobyteus/llm/user_message.py +73 -0
  105. autobyteus/llm/utils/llm_config.py +124 -27
  106. autobyteus/llm/utils/response_types.py +3 -2
  107. autobyteus/llm/utils/token_usage.py +7 -4
  108. autobyteus/rpc/__init__.py +73 -0
  109. autobyteus/rpc/client/__init__.py +17 -0
  110. autobyteus/rpc/client/abstract_client_connection.py +124 -0
  111. autobyteus/rpc/client/client_connection_manager.py +153 -0
  112. autobyteus/rpc/client/sse_client_connection.py +306 -0
  113. autobyteus/rpc/client/stdio_client_connection.py +280 -0
  114. autobyteus/rpc/config/__init__.py +13 -0
  115. autobyteus/rpc/config/agent_server_config.py +153 -0
  116. autobyteus/rpc/config/agent_server_registry.py +152 -0
  117. autobyteus/rpc/hosting.py +244 -0
  118. autobyteus/rpc/protocol.py +244 -0
  119. autobyteus/rpc/server/__init__.py +20 -0
  120. autobyteus/rpc/server/agent_server_endpoint.py +181 -0
  121. autobyteus/rpc/server/base_method_handler.py +40 -0
  122. autobyteus/rpc/server/method_handlers.py +259 -0
  123. autobyteus/rpc/server/sse_server_handler.py +182 -0
  124. autobyteus/rpc/server/stdio_server_handler.py +151 -0
  125. autobyteus/rpc/server_main.py +198 -0
  126. autobyteus/rpc/transport_type.py +13 -0
  127. autobyteus/tools/__init__.py +75 -0
  128. autobyteus/tools/ask_user_input.py +34 -77
  129. autobyteus/tools/base_tool.py +66 -37
  130. autobyteus/tools/bash/__init__.py +2 -0
  131. autobyteus/tools/bash/bash_executor.py +42 -79
  132. autobyteus/tools/browser/__init__.py +2 -0
  133. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +50 -42
  134. autobyteus/tools/browser/session_aware/browser_session_aware_tool.py +7 -4
  135. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +117 -125
  136. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +75 -22
  137. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +94 -28
  138. autobyteus/tools/browser/session_aware/factory/browser_session_aware_web_element_trigger_factory.py +10 -2
  139. autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_reader_factory.py +18 -2
  140. autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_screenshot_taker_factory.py +10 -2
  141. autobyteus/tools/browser/session_aware/shared_browser_session_manager.py +4 -3
  142. autobyteus/tools/browser/standalone/__init__.py +7 -0
  143. autobyteus/tools/browser/standalone/factory/google_search_factory.py +17 -2
  144. autobyteus/tools/browser/standalone/factory/webpage_reader_factory.py +17 -2
  145. autobyteus/tools/browser/standalone/factory/webpage_screenshot_taker_factory.py +10 -2
  146. autobyteus/tools/browser/standalone/google_search_ui.py +104 -67
  147. autobyteus/tools/browser/standalone/navigate_to.py +52 -28
  148. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +94 -0
  149. autobyteus/tools/browser/standalone/webpage_image_downloader.py +146 -61
  150. autobyteus/tools/browser/standalone/webpage_reader.py +80 -61
  151. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +91 -45
  152. autobyteus/tools/factory/__init__.py +9 -0
  153. autobyteus/tools/factory/tool_factory.py +25 -4
  154. autobyteus/tools/file/file_reader.py +22 -51
  155. autobyteus/tools/file/file_writer.py +25 -56
  156. autobyteus/tools/functional_tool.py +234 -0
  157. autobyteus/tools/image_downloader.py +49 -71
  158. autobyteus/tools/mcp/__init__.py +47 -0
  159. autobyteus/tools/mcp/call_handlers/__init__.py +18 -0
  160. autobyteus/tools/mcp/call_handlers/base_handler.py +40 -0
  161. autobyteus/tools/mcp/call_handlers/sse_handler.py +22 -0
  162. autobyteus/tools/mcp/call_handlers/stdio_handler.py +62 -0
  163. autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +55 -0
  164. autobyteus/tools/mcp/config_service.py +258 -0
  165. autobyteus/tools/mcp/factory.py +70 -0
  166. autobyteus/tools/mcp/registrar.py +135 -0
  167. autobyteus/tools/mcp/schema_mapper.py +131 -0
  168. autobyteus/tools/mcp/tool.py +101 -0
  169. autobyteus/tools/mcp/types.py +96 -0
  170. autobyteus/tools/parameter_schema.py +268 -0
  171. autobyteus/tools/pdf_downloader.py +78 -79
  172. autobyteus/tools/registry/__init__.py +0 -2
  173. autobyteus/tools/registry/tool_definition.py +106 -34
  174. autobyteus/tools/registry/tool_registry.py +46 -22
  175. autobyteus/tools/timer.py +150 -102
  176. autobyteus/tools/tool_config.py +117 -0
  177. autobyteus/tools/tool_meta.py +48 -26
  178. autobyteus/tools/usage/__init__.py +6 -0
  179. autobyteus/tools/usage/formatters/__init__.py +31 -0
  180. autobyteus/tools/usage/formatters/anthropic_json_example_formatter.py +18 -0
  181. autobyteus/tools/usage/formatters/anthropic_json_schema_formatter.py +25 -0
  182. autobyteus/tools/usage/formatters/base_formatter.py +42 -0
  183. autobyteus/tools/usage/formatters/default_json_example_formatter.py +42 -0
  184. autobyteus/tools/usage/formatters/default_json_schema_formatter.py +28 -0
  185. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +55 -0
  186. autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +46 -0
  187. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +34 -0
  188. autobyteus/tools/usage/formatters/gemini_json_schema_formatter.py +25 -0
  189. autobyteus/tools/usage/formatters/google_json_example_formatter.py +34 -0
  190. autobyteus/tools/usage/formatters/google_json_schema_formatter.py +25 -0
  191. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +49 -0
  192. autobyteus/tools/usage/formatters/openai_json_schema_formatter.py +28 -0
  193. autobyteus/tools/usage/parsers/__init__.py +22 -0
  194. autobyteus/tools/usage/parsers/anthropic_xml_tool_usage_parser.py +10 -0
  195. autobyteus/tools/usage/parsers/base_parser.py +41 -0
  196. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +106 -0
  197. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +135 -0
  198. autobyteus/tools/usage/parsers/exceptions.py +13 -0
  199. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +68 -0
  200. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +147 -0
  201. autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +67 -0
  202. autobyteus/tools/usage/providers/__init__.py +22 -0
  203. autobyteus/tools/usage/providers/json_example_provider.py +32 -0
  204. autobyteus/tools/usage/providers/json_schema_provider.py +35 -0
  205. autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +28 -0
  206. autobyteus/tools/usage/providers/tool_manifest_provider.py +68 -0
  207. autobyteus/tools/usage/providers/xml_example_provider.py +28 -0
  208. autobyteus/tools/usage/providers/xml_schema_provider.py +29 -0
  209. autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +26 -0
  210. autobyteus/tools/usage/registries/__init__.py +20 -0
  211. autobyteus/tools/usage/registries/json_example_formatter_registry.py +51 -0
  212. autobyteus/tools/usage/registries/json_schema_formatter_registry.py +51 -0
  213. autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +42 -0
  214. autobyteus/tools/usage/registries/xml_example_formatter_registry.py +30 -0
  215. autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +33 -0
  216. autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +30 -0
  217. {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/METADATA +21 -3
  218. autobyteus-1.1.0.dist-info/RECORD +279 -0
  219. {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/WHEEL +1 -1
  220. autobyteus/agent/async_agent.py +0 -175
  221. autobyteus/agent/async_group_aware_agent.py +0 -136
  222. autobyteus/agent/group/async_group_aware_agent.py +0 -122
  223. autobyteus/agent/group/coordinator_agent.py +0 -36
  224. autobyteus/agent/group/group_aware_agent.py +0 -121
  225. autobyteus/agent/orchestrator/__init__.py +0 -0
  226. autobyteus/agent/orchestrator/base_agent_orchestrator.py +0 -82
  227. autobyteus/agent/orchestrator/multi_replica_agent_orchestrator.py +0 -72
  228. autobyteus/agent/orchestrator/single_replica_agent_orchestrator.py +0 -43
  229. autobyteus/agent/registry/__init__.py +0 -11
  230. autobyteus/agent/registry/agent_definition.py +0 -94
  231. autobyteus/agent/registry/agent_registry.py +0 -114
  232. autobyteus/agent/response_parser/__init__.py +0 -0
  233. autobyteus/agent/response_parser/tool_usage_command_parser.py +0 -100
  234. autobyteus/agent/status.py +0 -12
  235. autobyteus/conversation/__init__.py +0 -0
  236. autobyteus/conversation/conversation.py +0 -54
  237. autobyteus/conversation/user_message.py +0 -59
  238. autobyteus/events/decorators.py +0 -29
  239. autobyteus/prompt/prompt_version_manager.py +0 -58
  240. autobyteus/prompt/storage/__init__.py +0 -0
  241. autobyteus/prompt/storage/prompt_version_model.py +0 -29
  242. autobyteus/prompt/storage/prompt_version_repository.py +0 -83
  243. autobyteus/tools/bash/factory/__init__.py +0 -0
  244. autobyteus/tools/bash/factory/bash_executor_factory.py +0 -6
  245. autobyteus/tools/factory/ask_user_input_factory.py +0 -6
  246. autobyteus/tools/factory/image_downloader_factory.py +0 -9
  247. autobyteus/tools/factory/pdf_downloader_factory.py +0 -9
  248. autobyteus/tools/factory/webpage_image_downloader_factory.py +0 -6
  249. autobyteus/tools/file/factory/__init__.py +0 -0
  250. autobyteus/tools/file/factory/file_reader_factory.py +0 -6
  251. autobyteus/tools/file/factory/file_writer_factory.py +0 -6
  252. autobyteus/tools/mcp_remote_tool.py +0 -82
  253. autobyteus/tools/web_page_pdf_generator.py +0 -90
  254. autobyteus-1.0.5.dist-info/RECORD +0 -163
  255. {autobyteus-1.0.5.dist-info → autobyteus-1.1.0.dist-info}/licenses/LICENSE +0 -0
  256. {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}'.")