autobyteus 1.1.0__py3-none-any.whl → 1.1.2__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 (103) hide show
  1. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +1 -1
  2. autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
  3. autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
  4. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
  5. autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
  6. autobyteus/agent/context/__init__.py +0 -5
  7. autobyteus/agent/context/agent_config.py +6 -2
  8. autobyteus/agent/context/agent_context.py +2 -5
  9. autobyteus/agent/context/agent_phase_manager.py +105 -5
  10. autobyteus/agent/context/agent_runtime_state.py +2 -2
  11. autobyteus/agent/context/phases.py +2 -0
  12. autobyteus/agent/events/__init__.py +0 -11
  13. autobyteus/agent/events/agent_events.py +0 -37
  14. autobyteus/agent/events/notifiers.py +25 -7
  15. autobyteus/agent/events/worker_event_dispatcher.py +1 -1
  16. autobyteus/agent/factory/agent_factory.py +6 -2
  17. autobyteus/agent/group/agent_group.py +16 -7
  18. autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
  19. autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
  20. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
  21. autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
  22. autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
  23. autobyteus/agent/hooks/__init__.py +7 -0
  24. autobyteus/agent/hooks/base_phase_hook.py +11 -2
  25. autobyteus/agent/hooks/hook_definition.py +36 -0
  26. autobyteus/agent/hooks/hook_meta.py +37 -0
  27. autobyteus/agent/hooks/hook_registry.py +118 -0
  28. autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
  29. autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
  30. autobyteus/agent/input_processor/processor_meta.py +1 -1
  31. autobyteus/agent/input_processor/processor_registry.py +19 -0
  32. autobyteus/agent/llm_response_processor/base_processor.py +6 -3
  33. autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
  34. autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
  35. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
  36. autobyteus/agent/message/context_file_type.py +2 -3
  37. autobyteus/agent/phases/__init__.py +18 -0
  38. autobyteus/agent/phases/discover.py +52 -0
  39. autobyteus/agent/phases/manager.py +265 -0
  40. autobyteus/agent/phases/phase_enum.py +49 -0
  41. autobyteus/agent/phases/transition_decorator.py +40 -0
  42. autobyteus/agent/phases/transition_info.py +33 -0
  43. autobyteus/agent/remote_agent.py +1 -1
  44. autobyteus/agent/runtime/agent_runtime.py +5 -10
  45. autobyteus/agent/runtime/agent_worker.py +62 -19
  46. autobyteus/agent/streaming/agent_event_stream.py +58 -5
  47. autobyteus/agent/streaming/stream_event_payloads.py +24 -13
  48. autobyteus/agent/streaming/stream_events.py +14 -11
  49. autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
  50. autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
  51. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
  52. autobyteus/agent/tool_invocation.py +29 -3
  53. autobyteus/agent/utils/wait_for_idle.py +1 -1
  54. autobyteus/agent/workspace/__init__.py +2 -0
  55. autobyteus/agent/workspace/base_workspace.py +33 -11
  56. autobyteus/agent/workspace/workspace_config.py +160 -0
  57. autobyteus/agent/workspace/workspace_definition.py +36 -0
  58. autobyteus/agent/workspace/workspace_meta.py +37 -0
  59. autobyteus/agent/workspace/workspace_registry.py +72 -0
  60. autobyteus/cli/__init__.py +4 -3
  61. autobyteus/cli/agent_cli.py +25 -207
  62. autobyteus/cli/cli_display.py +205 -0
  63. autobyteus/events/event_manager.py +2 -1
  64. autobyteus/events/event_types.py +3 -1
  65. autobyteus/llm/api/autobyteus_llm.py +2 -12
  66. autobyteus/llm/api/deepseek_llm.py +11 -173
  67. autobyteus/llm/api/grok_llm.py +11 -172
  68. autobyteus/llm/api/kimi_llm.py +24 -0
  69. autobyteus/llm/api/mistral_llm.py +4 -4
  70. autobyteus/llm/api/ollama_llm.py +2 -2
  71. autobyteus/llm/api/openai_compatible_llm.py +193 -0
  72. autobyteus/llm/api/openai_llm.py +11 -139
  73. autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
  74. autobyteus/llm/llm_factory.py +168 -42
  75. autobyteus/llm/models.py +25 -29
  76. autobyteus/llm/ollama_provider.py +6 -2
  77. autobyteus/llm/ollama_provider_resolver.py +44 -0
  78. autobyteus/llm/providers.py +1 -0
  79. autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
  80. autobyteus/llm/token_counter/token_counter_factory.py +3 -0
  81. autobyteus/llm/utils/messages.py +3 -3
  82. autobyteus/tools/__init__.py +2 -0
  83. autobyteus/tools/base_tool.py +7 -1
  84. autobyteus/tools/functional_tool.py +20 -5
  85. autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
  86. autobyteus/tools/mcp/config_service.py +106 -127
  87. autobyteus/tools/mcp/registrar.py +247 -59
  88. autobyteus/tools/mcp/types.py +5 -3
  89. autobyteus/tools/registry/tool_definition.py +8 -1
  90. autobyteus/tools/registry/tool_registry.py +18 -0
  91. autobyteus/tools/tool_category.py +11 -0
  92. autobyteus/tools/tool_meta.py +3 -1
  93. autobyteus/tools/tool_state.py +20 -0
  94. autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
  95. autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
  96. autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
  97. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
  98. autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
  99. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
  100. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
  101. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
  102. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
  103. {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
@@ -60,6 +60,25 @@ class LLMResponseProcessorRegistry(metaclass=SingletonMeta):
60
60
  logger.debug(f"LLM response processor definition with name '{name}' not found in registry.")
61
61
  return definition
62
62
 
63
+ def get_processor(self, name: str) -> Optional['BaseLLMResponseProcessor']:
64
+ """
65
+ Retrieves an instance of an LLM response processor by its name.
66
+
67
+ Args:
68
+ name: The name of the LLM response processor to retrieve.
69
+
70
+ Returns:
71
+ An instance of the BaseLLMResponseProcessor if found and instantiable, otherwise None.
72
+ """
73
+ definition = self.get_processor_definition(name)
74
+ if definition:
75
+ try:
76
+ return definition.processor_class()
77
+ except Exception as e:
78
+ logger.error(f"Failed to instantiate LLM response processor '{name}' from class '{definition.processor_class.__name__}': {e}", exc_info=True)
79
+ return None
80
+ return None
81
+
63
82
  def list_processor_names(self) -> List[str]:
64
83
  """
65
84
  Returns a list of names of all registered LLM response processor definitions.
@@ -24,7 +24,8 @@ class ProviderAwareToolUsageProcessor(BaseLLMResponseProcessor):
24
24
  self._parser = ProviderAwareToolUsageParser()
25
25
  logger.debug("ProviderAwareToolUsageProcessor initialized.")
26
26
 
27
- def get_name(self) -> str:
27
+ @classmethod
28
+ def get_name(cls) -> str:
28
29
  return "provider_aware_tool_usage"
29
30
 
30
31
  async def process_response(self, response: 'CompleteResponse', context: 'AgentContext', triggering_event: 'LLMCompleteResponseReceivedEvent') -> bool:
@@ -1,4 +1,3 @@
1
- # file: autobyteus/autobyteus/agent/message/context_file_type.py
2
1
  from enum import Enum
3
2
  import os
4
3
 
@@ -18,7 +17,7 @@ class ContextFileType(str, Enum):
18
17
  HTML = "html" # .html, .htm
19
18
  PYTHON = "python" # .py
20
19
  JAVASCRIPT = "javascript" # .js
21
- IMAGE_CONTEXT = "image_context" # .png, .jpg, .jpeg, .gif, .webp (when image is for contextual analysis, not direct LLM vision input)
20
+ IMAGE = "image" # .png, .jpg, .jpeg, .gif, .webp (when image is for contextual analysis, not direct LLM vision input)
22
21
  UNKNOWN = "unknown" # Fallback for unrecognized types
23
22
 
24
23
  @classmethod
@@ -56,7 +55,7 @@ class ContextFileType(str, Enum):
56
55
  elif extension == ".js":
57
56
  return cls.JAVASCRIPT
58
57
  elif extension in [".png", ".jpg", ".jpeg", ".gif", ".webp"]:
59
- return cls.IMAGE_CONTEXT
58
+ return cls.IMAGE
60
59
  else:
61
60
  return cls.UNKNOWN
62
61
 
@@ -0,0 +1,18 @@
1
+ # file: autobyteus/autobyteus/agent/phases/__init__.py
2
+ """
3
+ This package contains components for defining and describing agent operational phases
4
+ and their transitions.
5
+ """
6
+ from .phase_enum import AgentOperationalPhase
7
+ from .transition_info import PhaseTransitionInfo
8
+ from .transition_decorator import phase_transition
9
+ from .discover import PhaseTransitionDiscoverer
10
+ from .manager import AgentPhaseManager
11
+
12
+ __all__ = [
13
+ "AgentOperationalPhase",
14
+ "PhaseTransitionInfo",
15
+ "phase_transition",
16
+ "PhaseTransitionDiscoverer",
17
+ "AgentPhaseManager",
18
+ ]
@@ -0,0 +1,52 @@
1
+ # file: autobyteus/autobyteus/agent/phases/discover.py
2
+ import inspect
3
+ import logging
4
+ from typing import List, Optional
5
+
6
+ from autobyteus.agent.context.agent_phase_manager import AgentPhaseManager
7
+ from .transition_info import PhaseTransitionInfo
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class PhaseTransitionDiscoverer:
12
+ """
13
+ A utility class to discover all valid phase transitions within the system.
14
+
15
+ It works by introspecting the AgentPhaseManager and finding methods
16
+ that have been decorated with the `@phase_transition` decorator.
17
+ """
18
+ _cached_transitions: Optional[List[PhaseTransitionInfo]] = None
19
+
20
+ @classmethod
21
+ def discover(cls) -> List[PhaseTransitionInfo]:
22
+ """
23
+ Discovers and returns a list of all possible phase transitions.
24
+
25
+ The result is cached after the first call for performance.
26
+
27
+ Returns:
28
+ A list of PhaseTransitionInfo objects, each describing a valid transition.
29
+ """
30
+ if cls._cached_transitions is not None:
31
+ return cls._cached_transitions
32
+
33
+ logger.debug("Discovering phase transitions from AgentPhaseManager for the first time.")
34
+ transitions = []
35
+ for name, method in inspect.getmembers(AgentPhaseManager, predicate=inspect.isfunction):
36
+ if hasattr(method, '_transition_info'):
37
+ info = getattr(method, '_transition_info')
38
+ if isinstance(info, PhaseTransitionInfo):
39
+ transitions.append(info)
40
+
41
+ # Sort for deterministic output
42
+ transitions.sort(key=lambda t: (t.target_phase.value, t.triggering_method))
43
+
44
+ cls._cached_transitions = transitions
45
+ logger.info(f"Discovered and cached {len(transitions)} phase transitions.")
46
+ return transitions
47
+
48
+ @classmethod
49
+ def clear_cache(cls) -> None:
50
+ """Clears the cached list of transitions."""
51
+ cls._cached_transitions = None
52
+ logger.info("Cleared cached phase transitions.")
@@ -0,0 +1,265 @@
1
+ # file: autobyteus/autobyteus/agent/phases/manager.py
2
+ import asyncio
3
+ import logging
4
+ from typing import TYPE_CHECKING, Optional, Dict, Any
5
+
6
+ from .phase_enum import AgentOperationalPhase
7
+ from .transition_decorator import phase_transition
8
+
9
+ if TYPE_CHECKING:
10
+ from autobyteus.agent.context import AgentContext
11
+ from autobyteus.agent.tool_invocation import ToolInvocation
12
+ from autobyteus.agent.events.notifiers import AgentExternalEventNotifier
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class AgentPhaseManager:
18
+ """
19
+ Manages the operational phase of an agent, uses an AgentExternalEventNotifier
20
+ to signal phase changes externally, and executes phase transition hooks.
21
+ """
22
+ def __init__(self, context: 'AgentContext', notifier: 'AgentExternalEventNotifier'):
23
+ self.context: 'AgentContext' = context
24
+ self.notifier: 'AgentExternalEventNotifier' = notifier
25
+
26
+ self.context.current_phase = AgentOperationalPhase.UNINITIALIZED
27
+
28
+ logger.debug(f"AgentPhaseManager initialized for agent_id '{self.context.agent_id}'. "
29
+ f"Initial phase: {self.context.current_phase.value}. Uses provided notifier.")
30
+
31
+ async def _execute_hooks(self, old_phase: AgentOperationalPhase, new_phase: AgentOperationalPhase):
32
+ """Asynchronously executes hooks that match the given phase transition."""
33
+ hooks_to_run = [
34
+ hook for hook in self.context.config.phase_hooks
35
+ if hook.source_phase == old_phase and hook.target_phase == new_phase
36
+ ]
37
+
38
+ if not hooks_to_run:
39
+ return
40
+
41
+ hook_names = [hook.__class__.__name__ for hook in hooks_to_run]
42
+ logger.info(f"Agent '{self.context.agent_id}': Executing {len(hooks_to_run)} hooks for transition "
43
+ f"'{old_phase.value}' -> '{new_phase.value}': {hook_names}")
44
+
45
+ for hook in hooks_to_run:
46
+ try:
47
+ await hook.execute(self.context)
48
+ logger.debug(f"Agent '{self.context.agent_id}': Hook '{hook.__class__.__name__}' executed successfully.")
49
+ except Exception as e:
50
+ logger.error(f"Agent '{self.context.agent_id}': Error executing phase transition hook "
51
+ f"'{hook.__class__.__name__}' for '{old_phase.value}' -> '{new_phase.value}': {e}",
52
+ exc_info=True)
53
+ # We log the error but do not halt the agent's phase transition.
54
+
55
+ async def _transition_phase(self, new_phase: AgentOperationalPhase,
56
+ notify_method_name: str,
57
+ additional_data: Optional[Dict[str, Any]] = None):
58
+ """
59
+ Private async helper to change the agent's phase, execute hooks, and then
60
+ call the appropriate notifier method. Hooks are now awaited.
61
+ """
62
+ if not isinstance(new_phase, AgentOperationalPhase):
63
+ logger.error(f"AgentPhaseManager for '{self.context.agent_id}' received invalid type for new_phase: {type(new_phase)}. Must be AgentOperationalPhase.")
64
+ return
65
+
66
+ old_phase = self.context.current_phase
67
+
68
+ if old_phase == new_phase:
69
+ logger.debug(f"AgentPhaseManager for '{self.context.agent_id}': already in phase {new_phase.value}. No transition.")
70
+ return
71
+
72
+ logger.info(f"Agent '{self.context.agent_id}' phase transitioning from {old_phase.value} to {new_phase.value}.")
73
+ self.context.current_phase = new_phase
74
+
75
+ # Execute and wait for hooks to complete *before* notifying externally.
76
+ await self._execute_hooks(old_phase, new_phase)
77
+
78
+ notifier_method = getattr(self.notifier, notify_method_name, None)
79
+ if notifier_method and callable(notifier_method):
80
+ notify_args = {"old_phase": old_phase}
81
+ if additional_data:
82
+ notify_args.update(additional_data)
83
+
84
+ notifier_method(**notify_args)
85
+ else:
86
+ logger.error(f"AgentPhaseManager for '{self.context.agent_id}': Notifier method '{notify_method_name}' not found or not callable on {type(self.notifier).__name__}.")
87
+
88
+ @phase_transition(
89
+ source_phases=[AgentOperationalPhase.SHUTDOWN_COMPLETE, AgentOperationalPhase.ERROR],
90
+ target_phase=AgentOperationalPhase.UNINITIALIZED,
91
+ description="Triggered when the agent runtime is started or restarted after being in a terminal state."
92
+ )
93
+ async def notify_runtime_starting_and_uninitialized(self) -> None:
94
+ if self.context.current_phase == AgentOperationalPhase.UNINITIALIZED:
95
+ await self._transition_phase(AgentOperationalPhase.UNINITIALIZED, "notify_phase_uninitialized_entered")
96
+ elif self.context.current_phase.is_terminal():
97
+ await self._transition_phase(AgentOperationalPhase.UNINITIALIZED, "notify_phase_uninitialized_entered")
98
+ else:
99
+ logger.warning(f"Agent '{self.context.agent_id}' notify_runtime_starting_and_uninitialized called in unexpected phase: {self.context.current_phase.value}")
100
+
101
+ @phase_transition(
102
+ source_phases=[AgentOperationalPhase.UNINITIALIZED],
103
+ target_phase=AgentOperationalPhase.BOOTSTRAPPING,
104
+ description="Occurs when the agent's internal bootstrapping process begins."
105
+ )
106
+ async def notify_bootstrapping_started(self) -> None:
107
+ await self._transition_phase(AgentOperationalPhase.BOOTSTRAPPING, "notify_phase_bootstrapping_started")
108
+
109
+ @phase_transition(
110
+ source_phases=[AgentOperationalPhase.BOOTSTRAPPING],
111
+ target_phase=AgentOperationalPhase.IDLE,
112
+ description="Occurs when the agent successfully completes bootstrapping and is ready for input."
113
+ )
114
+ async def notify_initialization_complete(self) -> None:
115
+ if self.context.current_phase.is_initializing() or self.context.current_phase == AgentOperationalPhase.UNINITIALIZED:
116
+ # This will now be a BOOTSTRAPPING -> IDLE transition
117
+ await self._transition_phase(AgentOperationalPhase.IDLE, "notify_phase_idle_entered")
118
+ else:
119
+ logger.warning(f"Agent '{self.context.agent_id}' notify_initialization_complete called in unexpected phase: {self.context.current_phase.value}")
120
+
121
+ @phase_transition(
122
+ source_phases=[
123
+ AgentOperationalPhase.IDLE, AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
124
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.EXECUTING_TOOL,
125
+ AgentOperationalPhase.TOOL_DENIED
126
+ ],
127
+ target_phase=AgentOperationalPhase.PROCESSING_USER_INPUT,
128
+ description="Fires when the agent begins processing a new user message or inter-agent message."
129
+ )
130
+ async def notify_processing_input_started(self, trigger_info: Optional[str] = None) -> None:
131
+ if self.context.current_phase in [AgentOperationalPhase.IDLE, AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.EXECUTING_TOOL, AgentOperationalPhase.TOOL_DENIED]:
132
+ data = {"trigger_info": trigger_info} if trigger_info else {}
133
+ await self._transition_phase(AgentOperationalPhase.PROCESSING_USER_INPUT, "notify_phase_processing_user_input_started", additional_data=data)
134
+ elif self.context.current_phase == AgentOperationalPhase.PROCESSING_USER_INPUT:
135
+ logger.debug(f"Agent '{self.context.agent_id}' already in PROCESSING_USER_INPUT phase.")
136
+ else:
137
+ logger.warning(f"Agent '{self.context.agent_id}' notify_processing_input_started called in unexpected phase: {self.context.current_phase.value}")
138
+
139
+ @phase_transition(
140
+ source_phases=[AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.PROCESSING_TOOL_RESULT],
141
+ target_phase=AgentOperationalPhase.AWAITING_LLM_RESPONSE,
142
+ description="Occurs just before the agent makes a call to the LLM."
143
+ )
144
+ async def notify_awaiting_llm_response(self) -> None:
145
+ await self._transition_phase(AgentOperationalPhase.AWAITING_LLM_RESPONSE, "notify_phase_awaiting_llm_response_started")
146
+
147
+ @phase_transition(
148
+ source_phases=[AgentOperationalPhase.AWAITING_LLM_RESPONSE],
149
+ target_phase=AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
150
+ description="Occurs after the agent has received a complete response from the LLM and begins to analyze it."
151
+ )
152
+ async def notify_analyzing_llm_response(self) -> None:
153
+ await self._transition_phase(AgentOperationalPhase.ANALYZING_LLM_RESPONSE, "notify_phase_analyzing_llm_response_started")
154
+
155
+ @phase_transition(
156
+ source_phases=[AgentOperationalPhase.ANALYZING_LLM_RESPONSE],
157
+ target_phase=AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
158
+ description="Occurs if the agent proposes a tool use that requires manual user approval."
159
+ )
160
+ async def notify_tool_execution_pending_approval(self, tool_invocation: 'ToolInvocation') -> None:
161
+ await self._transition_phase(AgentOperationalPhase.AWAITING_TOOL_APPROVAL, "notify_phase_awaiting_tool_approval_started")
162
+
163
+ @phase_transition(
164
+ source_phases=[AgentOperationalPhase.AWAITING_TOOL_APPROVAL],
165
+ target_phase=AgentOperationalPhase.EXECUTING_TOOL,
166
+ description="Occurs after a pending tool use has been approved and is about to be executed."
167
+ )
168
+ async def notify_tool_execution_resumed_after_approval(self, approved: bool, tool_name: Optional[str]) -> None:
169
+ if approved and tool_name:
170
+ await self._transition_phase(AgentOperationalPhase.EXECUTING_TOOL, "notify_phase_executing_tool_started", additional_data={"tool_name": tool_name})
171
+ else:
172
+ logger.info(f"Agent '{self.context.agent_id}' tool execution denied for '{tool_name}'. Transitioning to allow LLM to process denial.")
173
+ await self.notify_tool_denied(tool_name)
174
+
175
+ @phase_transition(
176
+ source_phases=[AgentOperationalPhase.AWAITING_TOOL_APPROVAL],
177
+ target_phase=AgentOperationalPhase.TOOL_DENIED,
178
+ description="Occurs after a pending tool use has been denied by the user."
179
+ )
180
+ async def notify_tool_denied(self, tool_name: Optional[str]) -> None:
181
+ """Notifies that a tool execution has been denied."""
182
+ await self._transition_phase(
183
+ AgentOperationalPhase.TOOL_DENIED,
184
+ "notify_phase_tool_denied_started",
185
+ additional_data={"tool_name": tool_name, "denial_for_tool": tool_name}
186
+ )
187
+
188
+ @phase_transition(
189
+ source_phases=[AgentOperationalPhase.ANALYZING_LLM_RESPONSE],
190
+ target_phase=AgentOperationalPhase.EXECUTING_TOOL,
191
+ description="Occurs when an agent with auto-approval executes a tool."
192
+ )
193
+ async def notify_tool_execution_started(self, tool_name: str) -> None:
194
+ await self._transition_phase(AgentOperationalPhase.EXECUTING_TOOL, "notify_phase_executing_tool_started", additional_data={"tool_name": tool_name})
195
+
196
+ @phase_transition(
197
+ source_phases=[AgentOperationalPhase.EXECUTING_TOOL],
198
+ target_phase=AgentOperationalPhase.PROCESSING_TOOL_RESULT,
199
+ description="Fires after a tool has finished executing and the agent begins processing its result."
200
+ )
201
+ async def notify_processing_tool_result(self, tool_name: str) -> None:
202
+ await self._transition_phase(AgentOperationalPhase.PROCESSING_TOOL_RESULT, "notify_phase_processing_tool_result_started", additional_data={"tool_name": tool_name})
203
+
204
+ @phase_transition(
205
+ source_phases=[
206
+ AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
207
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT
208
+ ],
209
+ target_phase=AgentOperationalPhase.IDLE,
210
+ description="Occurs when an agent completes a processing cycle and is waiting for new input."
211
+ )
212
+ async def notify_processing_complete_and_idle(self) -> None:
213
+ if not self.context.current_phase.is_terminal() and self.context.current_phase != AgentOperationalPhase.IDLE:
214
+ await self._transition_phase(AgentOperationalPhase.IDLE, "notify_phase_idle_entered")
215
+ elif self.context.current_phase == AgentOperationalPhase.IDLE:
216
+ logger.debug(f"Agent '{self.context.agent_id}' processing complete, already IDLE.")
217
+ else:
218
+ logger.warning(f"Agent '{self.context.agent_id}' notify_processing_complete_and_idle called in unexpected phase: {self.context.current_phase.value}")
219
+
220
+ @phase_transition(
221
+ source_phases=[
222
+ AgentOperationalPhase.UNINITIALIZED, AgentOperationalPhase.BOOTSTRAPPING, AgentOperationalPhase.IDLE,
223
+ AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.AWAITING_LLM_RESPONSE,
224
+ AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
225
+ AgentOperationalPhase.TOOL_DENIED, AgentOperationalPhase.EXECUTING_TOOL,
226
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.SHUTTING_DOWN
227
+ ],
228
+ target_phase=AgentOperationalPhase.ERROR,
229
+ description="A catch-all transition that can occur from any non-terminal state if an unrecoverable error happens."
230
+ )
231
+ async def notify_error_occurred(self, error_message: str, error_details: Optional[str] = None) -> None:
232
+ if self.context.current_phase != AgentOperationalPhase.ERROR:
233
+ data = {"error_message": error_message, "error_details": error_details}
234
+ await self._transition_phase(AgentOperationalPhase.ERROR, "notify_phase_error_entered", additional_data=data)
235
+ else:
236
+ logger.debug(f"Agent '{self.context.agent_id}' already in ERROR phase when another error notified: {error_message}")
237
+
238
+ @phase_transition(
239
+ source_phases=[
240
+ AgentOperationalPhase.UNINITIALIZED, AgentOperationalPhase.BOOTSTRAPPING, AgentOperationalPhase.IDLE,
241
+ AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.AWAITING_LLM_RESPONSE,
242
+ AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
243
+ AgentOperationalPhase.TOOL_DENIED, AgentOperationalPhase.EXECUTING_TOOL,
244
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT
245
+ ],
246
+ target_phase=AgentOperationalPhase.SHUTTING_DOWN,
247
+ description="Fires when the agent begins its graceful shutdown sequence."
248
+ )
249
+ async def notify_shutdown_initiated(self) -> None:
250
+ if not self.context.current_phase.is_terminal():
251
+ await self._transition_phase(AgentOperationalPhase.SHUTTING_DOWN, "notify_phase_shutting_down_started")
252
+ else:
253
+ logger.debug(f"Agent '{self.context.agent_id}' shutdown initiated but already in a terminal phase: {self.context.current_phase.value}")
254
+
255
+ @phase_transition(
256
+ source_phases=[AgentOperationalPhase.SHUTTING_DOWN],
257
+ target_phase=AgentOperationalPhase.SHUTDOWN_COMPLETE,
258
+ description="The final transition when the agent has successfully shut down and released its resources."
259
+ )
260
+ async def notify_final_shutdown_complete(self) -> None:
261
+ final_phase = AgentOperationalPhase.ERROR if self.context.current_phase == AgentOperationalPhase.ERROR else AgentOperationalPhase.SHUTDOWN_COMPLETE
262
+ if final_phase == AgentOperationalPhase.ERROR:
263
+ await self._transition_phase(AgentOperationalPhase.ERROR, "notify_phase_error_entered", additional_data={"error_message": "Shutdown completed with agent in error state."})
264
+ else:
265
+ await self._transition_phase(AgentOperationalPhase.SHUTDOWN_COMPLETE, "notify_phase_shutdown_completed")
@@ -0,0 +1,49 @@
1
+ # file: autobyteus/autobyteus/agent/context/phases/phase_enum.py
2
+ from enum import Enum
3
+
4
+ class AgentOperationalPhase(str, Enum):
5
+ """
6
+ Defines the fine-grained operational phases of an agent.
7
+ This is the single source of truth for an agent's current state of operation.
8
+ """
9
+ UNINITIALIZED = "uninitialized" # Agent object created, but runtime not started or fully set up.
10
+ BOOTSTRAPPING = "bootstrapping" # Agent is running its internal initialization/bootstrap sequence.
11
+ IDLE = "idle" # Fully initialized and ready for new input.
12
+
13
+ PROCESSING_USER_INPUT = "processing_user_input" # Actively processing a user message, typically preparing for an LLM call.
14
+ AWAITING_LLM_RESPONSE = "awaiting_llm_response" # Sent a request to LLM, waiting for the full response or stream.
15
+ ANALYZING_LLM_RESPONSE = "analyzing_llm_response" # Received LLM response, analyzing it for next actions (e.g., tool use, direct reply).
16
+
17
+ AWAITING_TOOL_APPROVAL = "awaiting_tool_approval" # Paused, needs external (user) approval for a tool invocation.
18
+ TOOL_DENIED = "tool_denied" # A proposed tool execution was denied by the user. Agent is processing the denial.
19
+ EXECUTING_TOOL = "executing_tool" # Tool has been approved (or auto-approved) and is currently running.
20
+ PROCESSING_TOOL_RESULT = "processing_tool_result" # Received a tool's result, actively processing it (often leading to another LLM call).
21
+
22
+ SHUTTING_DOWN = "shutting_down" # Shutdown sequence has been initiated.
23
+ SHUTDOWN_COMPLETE = "shutdown_complete" # Agent has fully stopped and released resources.
24
+ ERROR = "error" # An unrecoverable error has occurred. Agent might be non-operational.
25
+
26
+ def __str__(self) -> str:
27
+ return self.value
28
+
29
+ def is_initializing(self) -> bool:
30
+ """Checks if the agent is in any of the initializing phases."""
31
+ return self in [
32
+ AgentOperationalPhase.BOOTSTRAPPING,
33
+ ]
34
+
35
+ def is_processing(self) -> bool:
36
+ """Checks if the agent is in any active processing phase (post-initialization, pre-shutdown)."""
37
+ return self in [
38
+ AgentOperationalPhase.PROCESSING_USER_INPUT,
39
+ AgentOperationalPhase.AWAITING_LLM_RESPONSE,
40
+ AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
41
+ AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
42
+ AgentOperationalPhase.TOOL_DENIED,
43
+ AgentOperationalPhase.EXECUTING_TOOL,
44
+ AgentOperationalPhase.PROCESSING_TOOL_RESULT,
45
+ ]
46
+
47
+ def is_terminal(self) -> bool:
48
+ """Checks if the phase is a terminal state (shutdown or error)."""
49
+ return self in [AgentOperationalPhase.SHUTDOWN_COMPLETE, AgentOperationalPhase.ERROR]
@@ -0,0 +1,40 @@
1
+ # file: autobyteus/autobyteus/agent/phases/transition_decorator.py
2
+ import functools
3
+ from typing import List, Callable
4
+
5
+ from .phase_enum import AgentOperationalPhase
6
+ from .transition_info import PhaseTransitionInfo
7
+
8
+ def phase_transition(
9
+ source_phases: List[AgentOperationalPhase],
10
+ target_phase: AgentOperationalPhase,
11
+ description: str
12
+ ) -> Callable:
13
+ """
14
+ A decorator to annotate methods in AgentPhaseManager that cause a phase transition.
15
+
16
+ This decorator does not alter the method's execution. It attaches a
17
+ PhaseTransitionInfo object to the method, making the transition discoverable
18
+ via introspection.
19
+
20
+ Args:
21
+ source_phases: A list of valid source phases from which this transition can occur.
22
+ target_phase: The phase the agent will be in after this transition.
23
+ description: A human-readable description of what causes this transition.
24
+ """
25
+ def decorator(func: Callable) -> Callable:
26
+ @functools.wraps(func)
27
+ def wrapper(*args, **kwargs):
28
+ return func(*args, **kwargs)
29
+
30
+ # Attach the metadata to the function object itself.
31
+ # We sort source phases for consistent representation.
32
+ sorted_sources = tuple(sorted(source_phases, key=lambda p: p.value))
33
+ setattr(wrapper, '_transition_info', PhaseTransitionInfo(
34
+ source_phases=sorted_sources,
35
+ target_phase=target_phase,
36
+ description=description,
37
+ triggering_method=func.__name__
38
+ ))
39
+ return wrapper
40
+ return decorator
@@ -0,0 +1,33 @@
1
+ # file: autobyteus/autobyteus/agent/phases/transition_info.py
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import List, Tuple
5
+
6
+ from .phase_enum import AgentOperationalPhase
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @dataclass(frozen=True)
11
+ class PhaseTransitionInfo:
12
+ """
13
+ A dataclass representing a valid, discoverable phase transition.
14
+
15
+ This object provides the necessary metadata for users to understand what
16
+ kinds of phase hooks they can create.
17
+
18
+ Attributes:
19
+ source_phases: A list of possible source phases for this transition.
20
+ target_phase: The single target phase for this transition.
21
+ description: A human-readable description of when this transition occurs.
22
+ triggering_method: The name of the method in AgentPhaseManager that triggers this.
23
+ """
24
+ source_phases: Tuple[AgentOperationalPhase, ...]
25
+ target_phase: AgentOperationalPhase
26
+ description: str
27
+ triggering_method: str
28
+
29
+ def __repr__(self) -> str:
30
+ sources = ", ".join(f"'{p.value}'" for p in self.source_phases)
31
+ return (f"<PhaseTransitionInfo sources=[{sources}] -> "
32
+ f"target='{self.target_phase.value}' "
33
+ f"triggered_by='{self.triggering_method}'>")
@@ -5,7 +5,7 @@ import uuid # For generating default request IDs if ProtocolMessage doesn't
5
5
  from typing import Optional, Dict, Any, AsyncIterator
6
6
 
7
7
  from autobyteus.agent.agent import Agent
8
- from autobyteus.agent.context.phases import AgentOperationalPhase
8
+ from autobyteus.agent.phases import AgentOperationalPhase
9
9
  from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
10
10
  from autobyteus.agent.message.inter_agent_message import InterAgentMessage
11
11
  from autobyteus.rpc.client import default_client_connection_manager, AbstractClientConnection
@@ -5,11 +5,10 @@ import traceback
5
5
  import concurrent.futures
6
6
  from typing import Optional, Any, Callable, Awaitable, TYPE_CHECKING
7
7
 
8
- from autobyteus.agent.context.agent_context import AgentContext
9
- from autobyteus.agent.context.phases import AgentOperationalPhase
8
+ from autobyteus.agent.context import AgentContext
9
+ from autobyteus.agent.phases import AgentOperationalPhase, AgentPhaseManager
10
10
  from autobyteus.agent.events.notifiers import AgentExternalEventNotifier
11
11
  from autobyteus.agent.events import BaseEvent
12
- from autobyteus.agent.context.agent_phase_manager import AgentPhaseManager
13
12
  from autobyteus.agent.handlers import EventHandlerRegistry
14
13
  from autobyteus.agent.runtime.agent_worker import AgentWorker
15
14
 
@@ -85,9 +84,8 @@ class AgentRuntime:
85
84
  return
86
85
 
87
86
  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()
87
+ # The first meaningful phase change to BOOTSTRAPPING is triggered by the AgentBootstrapper
88
+ # within the worker's async context.
91
89
  self._worker.start()
92
90
  logger.info(f"AgentRuntime for '{agent_id}': Worker start command issued. Worker will initialize itself.")
93
91
 
@@ -122,10 +120,7 @@ class AgentRuntime:
122
120
  await self.phase_manager.notify_shutdown_initiated()
123
121
  await self._worker.stop(timeout=timeout)
124
122
 
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()
123
+ # LLM instance cleanup is now handled by the AgentWorker before its loop closes.
129
124
 
130
125
  await self.phase_manager.notify_final_shutdown_complete()
131
126
  logger.info(f"AgentRuntime for '{agent_id}' stop() method completed.")