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
@@ -6,7 +6,7 @@ import threading
6
6
  import concurrent.futures
7
7
  from typing import TYPE_CHECKING, Optional, Any, Callable, Awaitable, List
8
8
 
9
- from autobyteus.agent.context.phases import AgentOperationalPhase
9
+ from autobyteus.agent.phases import AgentOperationalPhase
10
10
  from autobyteus.agent.events import (
11
11
  BaseEvent,
12
12
  AgentErrorEvent,
@@ -140,19 +140,19 @@ class AgentWorker:
140
140
 
141
141
  async def async_run(self) -> None:
142
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
143
  try:
144
+ logger.info(f"AgentWorker '{agent_id}' async_run(): Starting.")
145
+
146
+ # --- Direct Initialization ---
147
+ initialization_successful = await self._initialize()
148
+ if not initialization_successful:
149
+ logger.critical(f"AgentWorker '{agent_id}' failed to initialize. Worker is shutting down.")
150
+ if self._async_stop_event and not self._async_stop_event.is_set():
151
+ self._async_stop_event.set()
152
+ return
153
+
154
+ # --- Main Event Loop ---
155
+ logger.info(f"AgentWorker '{agent_id}' initialized successfully. Entering main event loop.")
156
156
  while not self._async_stop_event.is_set():
157
157
  try:
158
158
  queue_event_tuple = await asyncio.wait_for(
@@ -183,18 +183,61 @@ class AgentWorker:
183
183
  if self.context.state.input_event_queues:
184
184
  await self.context.state.input_event_queues.enqueue_internal_system_event(AgentStoppedEvent())
185
185
 
186
+ async def _shutdown_sequence(self):
187
+ """
188
+ The explicit, ordered shutdown sequence for the worker, executed on its own event loop.
189
+ """
190
+ agent_id = self.context.agent_id
191
+ logger.info(f"AgentWorker '{agent_id}': Running shutdown sequence on worker loop.")
192
+
193
+ # 1. Clean up resources like the LLM instance.
194
+ if self.context.llm_instance and hasattr(self.context.llm_instance, 'cleanup'):
195
+ logger.info(f"AgentWorker '{agent_id}': Running LLM instance cleanup.")
196
+ try:
197
+ cleanup_func = self.context.llm_instance.cleanup
198
+ if asyncio.iscoroutinefunction(cleanup_func):
199
+ await cleanup_func()
200
+ else:
201
+ cleanup_func()
202
+ logger.info(f"AgentWorker '{agent_id}': LLM instance cleanup completed.")
203
+ except Exception as e:
204
+ logger.error(f"AgentWorker '{agent_id}': Error during LLM instance cleanup: {e}", exc_info=True)
205
+
206
+ # 2. Signal the main event loop to stop.
207
+ await self._signal_internal_stop()
208
+ logger.info(f"AgentWorker '{agent_id}': Shutdown sequence completed.")
209
+
186
210
  async def stop(self, timeout: float = 10.0) -> None:
211
+ """
212
+ Gracefully stops the worker by scheduling a final shutdown sequence on its
213
+ event loop, then waiting for the thread to terminate.
214
+ """
187
215
  if not self._is_active or self._stop_initiated:
188
216
  return
217
+
218
+ agent_id = self.context.agent_id
219
+ logger.info(f"AgentWorker '{agent_id}': Stop requested.")
189
220
  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
221
+
222
+ # Schedule the explicit shutdown sequence on the worker's loop.
223
+ if self.get_worker_loop():
224
+ future = self.schedule_coroutine_on_worker_loop(self._shutdown_sequence)
225
+ try:
226
+ # Wait for the cleanup and stop signal to be processed.
227
+ future.result(timeout=max(1.0, timeout-1))
228
+ except Exception as e:
229
+ logger.error(f"AgentWorker '{agent_id}': Error during scheduled shutdown sequence: {e}", exc_info=True)
230
+
231
+ # Wait for the main thread future to complete.
194
232
  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}'.")
233
+ try:
234
+ await asyncio.wait_for(asyncio.wrap_future(self._thread_future), timeout=timeout)
235
+ logger.info(f"AgentWorker '{agent_id}': Worker thread has terminated.")
236
+ except asyncio.TimeoutError:
237
+ logger.warning(f"AgentWorker '{agent_id}': Timeout waiting for worker thread to terminate.")
238
+
197
239
  self._is_active = False
198
240
 
241
+
199
242
  def is_alive(self) -> bool:
200
243
  return self._thread_future is not None and not self._thread_future.done()
@@ -1,3 +1,4 @@
1
+ # file: autobyteus/autobyteus/agent/streaming/agent_event_stream.py
1
2
  import asyncio
2
3
  import logging
3
4
  import traceback
@@ -14,9 +15,16 @@ from autobyteus.agent.streaming.stream_event_payloads import (
14
15
  create_agent_operational_phase_transition_data,
15
16
  create_error_event_data,
16
17
  create_tool_invocation_approval_requested_data,
18
+ create_tool_invocation_auto_executing_data,
19
+ AssistantChunkData,
20
+ AssistantCompleteResponseData,
21
+ ToolInteractionLogEntryData,
22
+ AgentOperationalPhaseTransitionData,
23
+ ToolInvocationApprovalRequestedData,
24
+ ToolInvocationAutoExecutingData,
25
+ ErrorEventData,
17
26
  EmptyData,
18
27
  StreamDataPayload,
19
- ErrorEventData,
20
28
  )
21
29
  from .queue_streamer import stream_queue_items
22
30
  from autobyteus.events.event_types import EventType
@@ -60,8 +68,7 @@ class AgentEventStream(EventEmitter):
60
68
  all_agent_event_types = [et for et in EventType if et.name.startswith("AGENT_")]
61
69
 
62
70
  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)
71
+ self.subscribe_from(self._notifier, event_type, self._handle_notifier_event_sync)
65
72
 
66
73
  def _handle_notifier_event_sync(self, event_type: EventType, payload: Optional[Any] = None, object_id: Optional[str] = None, **kwargs):
67
74
  event_agent_id = kwargs.get("agent_id", self.agent_id)
@@ -88,13 +95,15 @@ class AgentEventStream(EventEmitter):
88
95
  elif event_type == EventType.AGENT_REQUEST_TOOL_INVOCATION_APPROVAL:
89
96
  typed_payload_for_stream_event = create_tool_invocation_approval_requested_data(payload)
90
97
  stream_event_type_for_generic_stream = StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED
98
+ elif event_type == EventType.AGENT_TOOL_INVOCATION_AUTO_EXECUTING:
99
+ typed_payload_for_stream_event = create_tool_invocation_auto_executing_data(payload)
100
+ stream_event_type_for_generic_stream = StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING
91
101
  elif event_type == EventType.AGENT_ERROR_OUTPUT_GENERATION:
92
102
  typed_payload_for_stream_event = create_error_event_data(payload)
93
103
  stream_event_type_for_generic_stream = StreamEventType.ERROR_EVENT
94
104
 
95
- # The other queues are no longer needed, as `all_events` is the single source of truth.
96
105
  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.
106
+ pass
98
107
  else:
99
108
  logger.debug(f"AgentEventStream received internal event '{event_type.name}' with no direct stream mapping.")
100
109
 
@@ -118,3 +127,47 @@ class AgentEventStream(EventEmitter):
118
127
  """The primary method to consume all structured events from the agent."""
119
128
  async for event in stream_queue_items(self._generic_stream_event_internal_q, _AES_INTERNAL_SENTINEL, f"agent_{self.agent_id}_all_events"):
120
129
  yield event
130
+
131
+ # --- Convenience Stream Methods ---
132
+
133
+ async def stream_assistant_chunks(self) -> AsyncIterator[AssistantChunkData]:
134
+ """A convenience async generator that yields only assistant content/reasoning chunks."""
135
+ async for event in self.all_events():
136
+ if event.event_type == StreamEventType.ASSISTANT_CHUNK and isinstance(event.data, AssistantChunkData):
137
+ yield event.data
138
+
139
+ async def stream_assistant_final_response(self) -> AsyncIterator[AssistantCompleteResponseData]:
140
+ """A convenience async generator that yields only the final, complete assistant responses."""
141
+ async for event in self.all_events():
142
+ if event.event_type == StreamEventType.ASSISTANT_COMPLETE_RESPONSE and isinstance(event.data, AssistantCompleteResponseData):
143
+ yield event.data
144
+
145
+ async def stream_tool_logs(self) -> AsyncIterator[ToolInteractionLogEntryData]:
146
+ """A convenience async generator that yields only tool interaction log entries."""
147
+ async for event in self.all_events():
148
+ if event.event_type == StreamEventType.TOOL_INTERACTION_LOG_ENTRY and isinstance(event.data, ToolInteractionLogEntryData):
149
+ yield event.data
150
+
151
+ async def stream_phase_transitions(self) -> AsyncIterator[AgentOperationalPhaseTransitionData]:
152
+ """A convenience async generator that yields only agent phase transition data."""
153
+ async for event in self.all_events():
154
+ if event.event_type == StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION and isinstance(event.data, AgentOperationalPhaseTransitionData):
155
+ yield event.data
156
+
157
+ async def stream_tool_approval_requests(self) -> AsyncIterator[ToolInvocationApprovalRequestedData]:
158
+ """A convenience async generator that yields only requests for tool invocation approval."""
159
+ async for event in self.all_events():
160
+ if event.event_type == StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED and isinstance(event.data, ToolInvocationApprovalRequestedData):
161
+ yield event.data
162
+
163
+ async def stream_tool_auto_executing(self) -> AsyncIterator[ToolInvocationAutoExecutingData]:
164
+ """A convenience async generator that yields only events for tools being auto-executed."""
165
+ async for event in self.all_events():
166
+ if event.event_type == StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING and isinstance(event.data, ToolInvocationAutoExecutingData):
167
+ yield event.data
168
+
169
+ async def stream_errors(self) -> AsyncIterator[ErrorEventData]:
170
+ """A convenience async generator that yields only error events."""
171
+ async for event in self.all_events():
172
+ if event.event_type == StreamEventType.ERROR_EVENT and isinstance(event.data, ErrorEventData):
173
+ yield event.data
@@ -4,7 +4,7 @@ from typing import Dict, Any, Optional, List, Union
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
  from autobyteus.llm.utils.token_usage import TokenUsage
7
- from autobyteus.agent.context.phases import AgentOperationalPhase
7
+ from autobyteus.agent.phases import AgentOperationalPhase
8
8
 
9
9
 
10
10
  logger = logging.getLogger(__name__)
@@ -29,6 +29,8 @@ class AssistantCompleteResponseData(BaseStreamPayload):
29
29
 
30
30
  class ToolInteractionLogEntryData(BaseStreamPayload):
31
31
  log_entry: str
32
+ tool_invocation_id: str
33
+ tool_name: str
32
34
 
33
35
  class AgentOperationalPhaseTransitionData(BaseStreamPayload):
34
36
  new_phase: AgentOperationalPhase
@@ -48,17 +50,23 @@ class ToolInvocationApprovalRequestedData(BaseStreamPayload):
48
50
  tool_name: str
49
51
  arguments: Dict[str, Any]
50
52
 
53
+ class ToolInvocationAutoExecutingData(BaseStreamPayload):
54
+ invocation_id: str
55
+ tool_name: str
56
+ arguments: Dict[str, Any]
57
+
51
58
  class EmptyData(BaseStreamPayload):
52
59
  pass
53
60
 
54
61
  # Union of all possible data payload types
55
62
  StreamDataPayload = Union[
56
63
  AssistantChunkData,
57
- AssistantCompleteResponseData, # UPDATED in Union
64
+ AssistantCompleteResponseData,
58
65
  ToolInteractionLogEntryData,
59
66
  AgentOperationalPhaseTransitionData,
60
67
  ErrorEventData,
61
68
  ToolInvocationApprovalRequestedData,
69
+ ToolInvocationAutoExecutingData,
62
70
  EmptyData
63
71
  ]
64
72
 
@@ -99,8 +107,7 @@ def create_assistant_chunk_data(chunk_obj: Any) -> AssistantChunkData:
99
107
  )
100
108
  raise ValueError(f"Cannot create AssistantChunkData from {type(chunk_obj)}")
101
109
 
102
- # RENAMED FACTORY FUNCTION
103
- def create_assistant_complete_response_data(complete_resp_obj: Any) -> AssistantCompleteResponseData: # RENAMED
110
+ def create_assistant_complete_response_data(complete_resp_obj: Any) -> AssistantCompleteResponseData:
104
111
  usage_data = None
105
112
  if hasattr(complete_resp_obj, 'usage'):
106
113
  usage_data = getattr(complete_resp_obj, 'usage')
@@ -120,25 +127,24 @@ def create_assistant_complete_response_data(complete_resp_obj: Any) -> Assistant
120
127
  logger.warning(f"Unsupported usage type {type(usage_data)} for AssistantCompleteResponseData.")
121
128
 
122
129
  if hasattr(complete_resp_obj, 'content'):
123
- return AssistantCompleteResponseData( # Use new class name
130
+ return AssistantCompleteResponseData(
124
131
  content=str(getattr(complete_resp_obj, 'content', '')),
125
132
  reasoning=getattr(complete_resp_obj, 'reasoning', None),
126
133
  usage=parsed_usage
127
134
  )
128
135
  elif isinstance(complete_resp_obj, dict):
129
- return AssistantCompleteResponseData( # Use new class name
136
+ return AssistantCompleteResponseData(
130
137
  content=str(complete_resp_obj.get('content', '')),
131
138
  reasoning=complete_resp_obj.get('reasoning', None),
132
139
  usage=parsed_usage
133
140
  )
134
141
  raise ValueError(f"Cannot create AssistantCompleteResponseData from {type(complete_resp_obj)}")
135
142
 
136
- def create_tool_interaction_log_entry_data(log_entry: Any) -> ToolInteractionLogEntryData:
137
- if isinstance(log_entry, str):
138
- return ToolInteractionLogEntryData(log_entry=log_entry)
139
- elif isinstance(log_entry, dict) and 'log_entry' in log_entry:
140
- return ToolInteractionLogEntryData(log_entry=str(log_entry['log_entry']))
141
- raise ValueError(f"Cannot create ToolInteractionLogEntryData from {type(log_entry)}")
143
+ def create_tool_interaction_log_entry_data(log_data: Any) -> ToolInteractionLogEntryData:
144
+ if isinstance(log_data, dict):
145
+ if all(k in log_data for k in ['log_entry', 'tool_invocation_id', 'tool_name']):
146
+ return ToolInteractionLogEntryData(**log_data)
147
+ raise ValueError(f"Cannot create ToolInteractionLogEntryData from {type(log_data)}. Expected dict with 'log_entry', 'tool_invocation_id', and 'tool_name' keys.")
142
148
 
143
149
  def create_agent_operational_phase_transition_data(phase_data_dict: Any) -> AgentOperationalPhaseTransitionData:
144
150
  if isinstance(phase_data_dict, dict):
@@ -153,4 +159,9 @@ def create_error_event_data(error_data_dict: Any) -> ErrorEventData:
153
159
  def create_tool_invocation_approval_requested_data(approval_data_dict: Any) -> ToolInvocationApprovalRequestedData:
154
160
  if isinstance(approval_data_dict, dict):
155
161
  return ToolInvocationApprovalRequestedData(**approval_data_dict)
156
- raise ValueError(f"Cannot create ToolInvocationApprovalRequestedData from {type(approval_data_dict)}")
162
+ raise ValueError(f"Cannot create ToolInvocationApprovalRequestedData from {type(approval_data_dict)}")
163
+
164
+ def create_tool_invocation_auto_executing_data(auto_exec_data_dict: Any) -> ToolInvocationAutoExecutingData:
165
+ if isinstance(auto_exec_data_dict, dict):
166
+ return ToolInvocationAutoExecutingData(**auto_exec_data_dict)
167
+ raise ValueError(f"Cannot create ToolInvocationAutoExecutingData from {type(auto_exec_data_dict)}")
@@ -2,7 +2,7 @@
2
2
  import logging
3
3
  from enum import Enum
4
4
  from typing import Dict, Any, Optional, Union, Type
5
- from pydantic import BaseModel, Field, AwareDatetime, validator, RootModel
5
+ from pydantic import BaseModel, Field, AwareDatetime, field_validator, ValidationInfo
6
6
  import datetime
7
7
  import uuid
8
8
 
@@ -10,11 +10,12 @@ import uuid
10
10
  from .stream_event_payloads import (
11
11
  StreamDataPayload,
12
12
  AssistantChunkData,
13
- AssistantCompleteResponseData, # UPDATED import
13
+ AssistantCompleteResponseData,
14
14
  ToolInteractionLogEntryData,
15
15
  AgentOperationalPhaseTransitionData,
16
16
  ErrorEventData,
17
17
  ToolInvocationApprovalRequestedData,
18
+ ToolInvocationAutoExecutingData,
18
19
  EmptyData
19
20
  )
20
21
 
@@ -26,22 +27,24 @@ class StreamEventType(str, Enum):
26
27
  provided by AgentEventStream.
27
28
  """
28
29
  ASSISTANT_CHUNK = "assistant_chunk"
29
- ASSISTANT_COMPLETE_RESPONSE = "assistant_complete_response" # RENAMED from ASSISTANT_FINAL_MESSAGE
30
+ ASSISTANT_COMPLETE_RESPONSE = "assistant_complete_response"
30
31
  TOOL_INTERACTION_LOG_ENTRY = "tool_interaction_log_entry"
31
- AGENT_OPERATIONAL_PHASE_TRANSITION = "agent_operational_phase_transition" # RENAMED from AGENT_PHASE_UPDATE
32
+ AGENT_OPERATIONAL_PHASE_TRANSITION = "agent_operational_phase_transition"
32
33
  ERROR_EVENT = "error_event"
33
34
  TOOL_INVOCATION_APPROVAL_REQUESTED = "tool_invocation_approval_requested"
34
- AGENT_IDLE = "agent_idle" # ADDED: New event type for when agent becomes idle.
35
+ TOOL_INVOCATION_AUTO_EXECUTING = "tool_invocation_auto_executing"
36
+ AGENT_IDLE = "agent_idle"
35
37
 
36
38
 
37
39
  _STREAM_EVENT_TYPE_TO_PAYLOAD_CLASS: Dict[StreamEventType, Type[BaseModel]] = {
38
40
  StreamEventType.ASSISTANT_CHUNK: AssistantChunkData,
39
- StreamEventType.ASSISTANT_COMPLETE_RESPONSE: AssistantCompleteResponseData, # UPDATED mapping
41
+ StreamEventType.ASSISTANT_COMPLETE_RESPONSE: AssistantCompleteResponseData,
40
42
  StreamEventType.TOOL_INTERACTION_LOG_ENTRY: ToolInteractionLogEntryData,
41
- StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION: AgentOperationalPhaseTransitionData, # RENAMED mapping
43
+ StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION: AgentOperationalPhaseTransitionData,
42
44
  StreamEventType.ERROR_EVENT: ErrorEventData,
43
45
  StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED: ToolInvocationApprovalRequestedData,
44
- StreamEventType.AGENT_IDLE: AgentOperationalPhaseTransitionData, # ADDED: Mapped to the same payload as phase update
46
+ StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING: ToolInvocationAutoExecutingData,
47
+ StreamEventType.AGENT_IDLE: AgentOperationalPhaseTransitionData,
45
48
  }
46
49
 
47
50
 
@@ -72,9 +75,9 @@ class StreamEvent(BaseModel):
72
75
  description="Optional ID of the agent that originated this event."
73
76
  )
74
77
 
75
- @validator('data', pre=True, always=True)
76
- def validate_data_based_on_event_type(cls, v, values, **kwargs):
77
- event_type_value = values.get('event_type')
78
+ @field_validator('data', mode='before')
79
+ def validate_data_based_on_event_type(cls, v, info: ValidationInfo):
80
+ event_type_value = info.data.get('event_type')
78
81
  if not event_type_value:
79
82
  return v
80
83
 
@@ -3,23 +3,26 @@ import logging
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import TYPE_CHECKING, Dict
5
5
 
6
+ from .processor_meta import SystemPromptProcessorMeta
7
+
6
8
  if TYPE_CHECKING:
7
9
  from autobyteus.tools.base_tool import BaseTool
8
10
  from autobyteus.agent.context import AgentContext
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
- class BaseSystemPromptProcessor(ABC):
14
+ class BaseSystemPromptProcessor(ABC, metaclass=SystemPromptProcessorMeta):
13
15
  """
14
16
  Abstract base class for system prompt processors.
15
17
  Subclasses should be instantiated and passed to the AgentSpecification.
16
18
  """
17
- def get_name(self) -> str:
19
+ @classmethod
20
+ def get_name(cls) -> str:
18
21
  """
19
22
  Returns the unique name for this processor.
20
23
  Defaults to the class name. Can be overridden by subclasses.
21
24
  """
22
- return self.__class__.__name__
25
+ return cls.__name__
23
26
 
24
27
  @abstractmethod
25
28
  def process(self,
@@ -38,7 +38,7 @@ class SystemPromptProcessorMeta(ABCMeta):
38
38
  # Ensure 'cls' is correctly typed for SystemPromptProcessorDefinition
39
39
  definition = SystemPromptProcessorDefinition(name=processor_name, processor_class=cls) # type: ignore
40
40
  default_system_prompt_processor_registry.register_processor(definition)
41
- logger.info(f"Auto-registered system prompt processor: '{processor_name}' from class {name}")
41
+ logger.info(f"Auto-registered system prompt processor: '{processor_name}' from class {name} (no schema).")
42
42
 
43
43
  except AttributeError as e:
44
44
  # Catch if get_name is missing
@@ -5,6 +5,7 @@ from typing import Dict, TYPE_CHECKING, List
5
5
  from .base_processor import BaseSystemPromptProcessor
6
6
  from autobyteus.tools.registry import default_tool_registry, ToolDefinition
7
7
  from autobyteus.tools.usage.providers import ToolManifestProvider
8
+ from autobyteus.prompt.prompt_template import PromptTemplate
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from autobyteus.tools.base_tool import BaseTool
@@ -14,52 +15,65 @@ logger = logging.getLogger(__name__)
14
15
 
15
16
  class ToolManifestInjectorProcessor(BaseSystemPromptProcessor):
16
17
  """
17
- Injects a tool manifest into the system prompt, replacing '{{tools}}'.
18
- It delegates the generation of the manifest string to a ToolManifestProvider.
18
+ Injects a tool manifest into the system prompt using Jinja2-style placeholders.
19
+ It primarily targets the '{{tools}}' variable. It uses PromptTemplate for
20
+ rendering and delegates manifest generation to a ToolManifestProvider.
19
21
  """
20
- PLACEHOLDER = "{{tools}}"
22
+ # The '{{tools}}' placeholder is now handled by Jinja2 via PromptTemplate.
21
23
  DEFAULT_PREFIX_FOR_TOOLS_ONLY_PROMPT = "You have access to a set of tools. Use them by outputting the appropriate tool call format. The user can only see the output of the tool, not the call itself. The available tools are:\n\n"
22
24
 
23
25
  def __init__(self):
24
26
  self._manifest_provider = ToolManifestProvider()
25
27
  logger.debug(f"{self.get_name()} initialized.")
26
28
 
27
- def get_name(self) -> str:
29
+ @classmethod
30
+ def get_name(cls) -> str:
28
31
  return "ToolManifestInjector"
29
32
 
30
33
  def process(self, system_prompt: str, tool_instances: Dict[str, 'BaseTool'], agent_id: str, context: 'AgentContext') -> str:
31
- if self.PLACEHOLDER not in system_prompt:
34
+ try:
35
+ prompt_template = PromptTemplate(template=system_prompt)
36
+ except Exception as e:
37
+ logger.error(f"Failed to create PromptTemplate from system prompt for agent '{agent_id}'. Error: {e}", exc_info=True)
38
+ # Return original prompt on Jinja2 parsing failure
32
39
  return system_prompt
33
40
 
34
- is_tools_only_prompt = system_prompt.strip() == self.PLACEHOLDER
35
-
36
- if not tool_instances:
37
- logger.info(f"{self.get_name()}: The '{self.PLACEHOLDER}' placeholder is present, but no tools are instantiated. Replacing with 'No tools available.'")
38
- replacement_text = "No tools available for this agent."
39
- if is_tools_only_prompt:
40
- return self.DEFAULT_PREFIX_FOR_TOOLS_ONLY_PROMPT + replacement_text
41
- return system_prompt.replace(self.PLACEHOLDER, f"\n{replacement_text}")
41
+ # Check if the 'tools' variable is actually in the template
42
+ if "tools" not in prompt_template.required_vars:
43
+ return system_prompt
42
44
 
43
- tool_definitions: List[ToolDefinition] = [
44
- td for name in tool_instances if (td := default_tool_registry.get_tool_definition(name))
45
- ]
45
+ # Generate the manifest string for the 'tools' variable.
46
+ tools_manifest: str
47
+ if not tool_instances:
48
+ logger.info(f"{self.get_name()}: The '{{{{tools}}}}' placeholder is present, but no tools are instantiated. Using 'No tools available.'")
49
+ tools_manifest = "No tools available for this agent."
50
+ else:
51
+ tool_definitions: List[ToolDefinition] = [
52
+ td for name in tool_instances if (td := default_tool_registry.get_tool_definition(name))
53
+ ]
46
54
 
47
- llm_provider = context.llm_instance.model.provider if context.llm_instance and context.llm_instance.model else None
55
+ llm_provider = context.llm_instance.model.provider if context.llm_instance and context.llm_instance.model else None
48
56
 
49
- try:
50
- # Delegate manifest generation to the provider
51
- tools_description = self._manifest_provider.provide(
52
- tool_definitions=tool_definitions,
53
- use_xml=context.config.use_xml_tool_format,
54
- provider=llm_provider
55
- )
56
- except Exception as e:
57
- logger.exception(f"An unexpected error occurred during tool manifest generation for agent '{agent_id}': {e}")
58
- tools_description = "Error: Could not generate tool descriptions."
57
+ try:
58
+ # Delegate manifest generation to the provider
59
+ tools_manifest = self._manifest_provider.provide(
60
+ tool_definitions=tool_definitions,
61
+ use_xml=context.config.use_xml_tool_format,
62
+ provider=llm_provider
63
+ )
64
+ except Exception as e:
65
+ logger.exception(f"An unexpected error occurred during tool manifest generation for agent '{agent_id}': {e}")
66
+ tools_manifest = "Error: Could not generate tool descriptions."
59
67
 
60
- final_replacement_text = f"\n{tools_description}"
68
+ # Check if the prompt *only* contains the 'tools' variable by rendering with an empty string
69
+ rendered_without_tools = prompt_template.fill({"tools": ""})
70
+ is_tools_only_prompt = not rendered_without_tools.strip()
71
+
61
72
  if is_tools_only_prompt:
62
73
  logger.info(f"{self.get_name()}: Prompt contains only the tools placeholder. Prepending default instructions.")
63
- return self.DEFAULT_PREFIX_FOR_TOOLS_ONLY_PROMPT + tools_description
64
-
65
- return system_prompt.replace(self.PLACEHOLDER, final_replacement_text)
74
+ return self.DEFAULT_PREFIX_FOR_TOOLS_ONLY_PROMPT + tools_manifest
75
+ else:
76
+ # For prompts that contain other text, add a newline for better formatting before filling the template.
77
+ tools_description_with_newline = f"\n{tools_manifest}"
78
+ final_prompt = prompt_template.fill({"tools": tools_description_with_newline})
79
+ return final_prompt
@@ -1,5 +1,7 @@
1
1
  # file: autobyteus/autobyteus/agent/tool_invocation.py
2
2
  import uuid
3
+ import hashlib
4
+ import json
3
5
  from typing import Optional, Dict, Any
4
6
 
5
7
  class ToolInvocation:
@@ -11,11 +13,36 @@ class ToolInvocation:
11
13
  name: The name of the tool to be invoked.
12
14
  arguments: A dictionary of arguments for the tool.
13
15
  id: Optional. A unique identifier for this tool invocation.
14
- If None, a new UUID will be generated.
16
+ If None, a deterministic ID will be generated based on the tool name and arguments.
15
17
  """
16
18
  self.name: Optional[str] = name
17
19
  self.arguments: Optional[Dict[str, Any]] = arguments
18
- self.id: str = id if id is not None else str(uuid.uuid4())
20
+
21
+ if id is not None:
22
+ self.id: str = id
23
+ elif self.name is not None and self.arguments is not None:
24
+ self.id: str = self._generate_deterministic_id(self.name, self.arguments)
25
+ else:
26
+ # Fallback to UUID if name/args are not provided during init, though this is an edge case.
27
+ self.id: str = f"call_{uuid.uuid4().hex}"
28
+
29
+ @staticmethod
30
+ def _generate_deterministic_id(name: str, arguments: Dict[str, Any]) -> str:
31
+ """
32
+ Generates a deterministic ID for the tool invocation based on its content.
33
+ """
34
+ # Create a canonical representation of the arguments
35
+ # sort_keys=True ensures that the order of keys doesn't change the hash
36
+ canonical_args = json.dumps(arguments, sort_keys=True, separators=(',', ':'))
37
+
38
+ # Create a string to hash
39
+ hash_string = f"{name}:{canonical_args}"
40
+
41
+ # Use SHA256 for a robust hash
42
+ sha256_hash = hashlib.sha256(hash_string.encode('utf-8')).hexdigest()
43
+
44
+ # Prepend a prefix for clarity and use the full hash.
45
+ return f"call_{sha256_hash}"
19
46
 
20
47
  def is_valid(self) -> bool:
21
48
  """
@@ -27,4 +54,3 @@ class ToolInvocation:
27
54
  def __repr__(self) -> str:
28
55
  return (f"ToolInvocation(id='{self.id}', name='{self.name}', "
29
56
  f"arguments={self.arguments})")
30
-
@@ -6,7 +6,7 @@ from typing import Optional
6
6
  from autobyteus.agent.agent import Agent
7
7
  from autobyteus.agent.streaming.agent_event_stream import AgentEventStream
8
8
  from autobyteus.agent.streaming.stream_events import StreamEventType
9
- from autobyteus.agent.context.phases import AgentOperationalPhase
9
+ from autobyteus.agent.phases import AgentOperationalPhase
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
@@ -3,7 +3,9 @@
3
3
  Defines the agent's workspace or working environment.
4
4
  """
5
5
  from .base_workspace import BaseAgentWorkspace
6
+ from .workspace_config import WorkspaceConfig
6
7
 
7
8
  __all__ = [
8
9
  "BaseAgentWorkspace",
10
+ "WorkspaceConfig",
9
11
  ]