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
@@ -1,212 +1,41 @@
1
1
  import asyncio
2
2
  import logging
3
3
  import sys
4
- from typing import Optional, List, Dict, Any
5
- import json
4
+ from typing import Optional
6
5
 
7
6
  from autobyteus.agent.agent import Agent
8
- from autobyteus.agent.context.phases import AgentOperationalPhase
9
7
  from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
10
8
  from autobyteus.agent.streaming.agent_event_stream import AgentEventStream
11
- from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
12
- from autobyteus.agent.streaming.stream_event_payloads import (
13
- AssistantChunkData,
14
- AssistantCompleteResponseData,
15
- ToolInvocationApprovalRequestedData,
16
- ToolInteractionLogEntryData,
17
- AgentOperationalPhaseTransitionData,
18
- ErrorEventData,
19
- )
9
+ from .cli_display import InteractiveCLIDisplay
20
10
 
21
11
  logger = logging.getLogger(__name__)
22
12
 
23
- class InteractiveCLIManager:
24
- """
25
- Manages the state and rendering logic for the interactive CLI session.
26
- Input reading is handled by the main `run` loop. This class only handles output.
13
+ async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool = False, initial_prompt: Optional[str] = None):
27
14
  """
28
- def __init__(self, agent_turn_complete_event: asyncio.Event, show_tool_logs: bool, show_token_usage: bool):
29
- self.agent_turn_complete_event = agent_turn_complete_event
30
- self.show_tool_logs = show_tool_logs
31
- self.show_token_usage = show_token_usage
32
- self.current_line_empty = True
33
- self.agent_has_spoken_this_turn = False
34
- self.pending_approval_data: Optional[ToolInvocationApprovalRequestedData] = None
35
- self.is_thinking = False
36
- self.is_in_content_block = False
37
-
38
- def reset_turn_state(self):
39
- """Resets flags that are tracked on a per-turn basis."""
40
- self._end_thinking_block()
41
- self.agent_has_spoken_this_turn = False
42
- self.is_in_content_block = False
43
-
44
- def _ensure_new_line(self):
45
- """Ensures the cursor is on a new line if the current one isn't empty."""
46
- if not self.current_line_empty:
47
- sys.stdout.write("\n")
48
- sys.stdout.flush()
49
- self.current_line_empty = True
50
-
51
- def _end_thinking_block(self):
52
- """Closes the <Thinking> block if it was active."""
53
- if self.is_thinking:
54
- sys.stdout.write("\n</Thinking>")
55
- sys.stdout.flush()
56
- self.is_thinking = False
57
- self.current_line_empty = False
58
-
59
- def _display_tool_approval_prompt(self):
60
- """Displays the tool approval prompt using stored pending data."""
61
- if not self.pending_approval_data:
62
- return
63
-
64
- try:
65
- args_str = json.dumps(self.pending_approval_data.arguments, indent=2)
66
- except TypeError:
67
- args_str = str(self.pending_approval_data.arguments)
68
-
69
- self._ensure_new_line()
70
- prompt_message = (
71
- f"Tool Call: '{self.pending_approval_data.tool_name}' requests permission to run with arguments:\n"
72
- f"{args_str}\nApprove? (y/n): "
73
- )
74
- sys.stdout.write(prompt_message)
75
- sys.stdout.flush()
76
- self.current_line_empty = False
77
-
78
- async def handle_stream_event(self, event: StreamEvent):
79
- """Processes a single StreamEvent and updates the CLI display."""
80
- # A block of thinking ends if any event other than a reasoning chunk arrives.
81
- is_reasoning_only_chunk = (
82
- event.event_type == StreamEventType.ASSISTANT_CHUNK and
83
- isinstance(event.data, AssistantChunkData) and
84
- bool(event.data.reasoning) and not bool(event.data.content)
85
- )
86
- if not is_reasoning_only_chunk:
87
- self._end_thinking_block()
88
-
89
- # Most events should start on a new line.
90
- if event.event_type != StreamEventType.ASSISTANT_CHUNK:
91
- self._ensure_new_line()
92
-
93
- if event.event_type in [
94
- StreamEventType.AGENT_IDLE,
95
- StreamEventType.ERROR_EVENT,
96
- StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED,
97
- ]:
98
- self.agent_turn_complete_event.set()
99
-
100
- if event.event_type == StreamEventType.ASSISTANT_CHUNK and isinstance(event.data, AssistantChunkData):
101
- # If this is the first output from the agent this turn, print the "Agent: " prefix.
102
- if not self.agent_has_spoken_this_turn:
103
- self._ensure_new_line()
104
- sys.stdout.write("Agent:\n")
105
- sys.stdout.flush()
106
- self.agent_has_spoken_this_turn = True
107
- self.current_line_empty = True
108
-
109
- # Stream reasoning to stdout without logger formatting.
110
- if event.data.reasoning:
111
- if not self.is_thinking:
112
- sys.stdout.write("<Thinking>\n")
113
- sys.stdout.flush()
114
- self.is_thinking = True
115
- self.current_line_empty = True # We just printed a newline
116
-
117
- sys.stdout.write(event.data.reasoning)
118
- sys.stdout.flush()
119
- self.current_line_empty = event.data.reasoning.endswith('\n')
15
+ Runs an interactive command-line interface for a single agent.
120
16
 
121
- # Stream content to stdout.
122
- if event.data.content:
123
- if not self.is_in_content_block:
124
- self._ensure_new_line() # Ensures content starts on a new line after </Thinking>
125
- self.is_in_content_block = True
126
- sys.stdout.write(event.data.content)
127
- sys.stdout.flush()
128
- self.current_line_empty = event.data.content.endswith('\n')
129
-
130
- if self.show_token_usage and event.data.is_complete and event.data.usage:
131
- self._ensure_new_line()
132
- usage = event.data.usage
133
- logger.info(
134
- f"[Token Usage: Prompt={usage.prompt_tokens}, "
135
- f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
136
- )
137
-
138
- elif event.event_type == StreamEventType.ASSISTANT_COMPLETE_RESPONSE and isinstance(event.data, AssistantCompleteResponseData):
139
- # The reasoning has already been streamed. Do not log it again.
140
-
141
- if not self.agent_has_spoken_this_turn:
142
- # This case handles responses that might not have streamed any content chunks (e.g., only a tool call).
143
- # We still need to ensure the agent's turn is visibly terminated with a newline.
144
- self._ensure_new_line()
145
-
146
- # If there's final content that wasn't in a chunk, print it.
147
- if event.data.content:
148
- sys.stdout.write(f"Agent: {event.data.content}\n")
149
- sys.stdout.flush()
150
-
151
- if self.show_token_usage and event.data.usage:
152
- self._ensure_new_line()
153
- usage = event.data.usage
154
- logger.info(
155
- f"[Token Usage: Prompt={usage.prompt_tokens}, "
156
- f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
157
- )
158
-
159
- self.current_line_empty = True
160
- self.reset_turn_state() # Reset for next turn
17
+ This function orchestrates the agent's lifecycle, user input, and event streaming
18
+ for an interactive session.
161
19
 
162
- elif event.event_type == StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED and isinstance(event.data, ToolInvocationApprovalRequestedData):
163
- self.pending_approval_data = event.data
164
- self._display_tool_approval_prompt()
165
-
166
- elif event.event_type == StreamEventType.TOOL_INTERACTION_LOG_ENTRY and isinstance(event.data, ToolInteractionLogEntryData):
167
- if self.show_tool_logs:
168
- logger.info(f"[Tool Log: {event.data.log_entry}]")
169
-
170
- elif event.event_type == StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION and isinstance(event.data, AgentOperationalPhaseTransitionData):
171
- if event.data.new_phase == AgentOperationalPhase.EXECUTING_TOOL:
172
- tool_name = event.data.tool_name or "a tool"
173
- sys.stdout.write(f"Agent: Waiting for tool '{tool_name}' to complete...\n")
174
- sys.stdout.flush()
175
- self.current_line_empty = True
176
- self.agent_has_spoken_this_turn = True
177
- elif event.data.new_phase == AgentOperationalPhase.BOOTSTRAPPING:
178
- logger.info("[Agent is initializing...]")
179
- else:
180
- phase_msg = f"[Agent Status: {event.data.new_phase.value}"
181
- if event.data.tool_name:
182
- phase_msg += f" ({event.data.tool_name})"
183
- phase_msg += "]"
184
- logger.info(phase_msg)
185
-
186
- elif event.event_type == StreamEventType.ERROR_EVENT and isinstance(event.data, ErrorEventData):
187
- logger.error(f"[Error: {event.data.message} (Source: {event.data.source})]")
188
-
189
- elif event.event_type == StreamEventType.AGENT_IDLE:
190
- logger.info("[Agent is now idle.]")
191
-
192
- else:
193
- # Add logging for unhandled events for better debugging
194
- logger.debug(f"CLI Manager: Unhandled StreamEvent type: {event.event_type.value}")
195
-
196
-
197
- async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool = False, initial_prompt: Optional[str] = None):
20
+ Args:
21
+ agent: The agent instance to run.
22
+ show_tool_logs: If True, displays detailed logs from tool interactions.
23
+ show_token_usage: If True, displays token usage information after each LLM call.
24
+ initial_prompt: An optional initial prompt to send to the agent automatically.
25
+ """
198
26
  if not isinstance(agent, Agent):
199
27
  raise TypeError(f"Expected an Agent instance, got {type(agent).__name__}")
200
28
 
201
29
  logger.info(f"Starting interactive CLI session for agent '{agent.agent_id}'.")
202
30
  agent_turn_complete_event = asyncio.Event()
203
- cli_manager = InteractiveCLIManager(agent_turn_complete_event, show_tool_logs, show_token_usage)
31
+ cli_display = InteractiveCLIDisplay(agent_turn_complete_event, show_tool_logs, show_token_usage)
204
32
  streamer = AgentEventStream(agent)
205
33
 
206
34
  async def process_agent_events():
35
+ """Task to continuously process and display events from the agent."""
207
36
  try:
208
37
  async for event in streamer.all_events():
209
- await cli_manager.handle_stream_event(event)
38
+ await cli_display.handle_stream_event(event)
210
39
  except asyncio.CancelledError:
211
40
  logger.info("CLI event processing task cancelled.")
212
41
  except Exception as e:
@@ -226,35 +55,29 @@ async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool
226
55
  await asyncio.wait_for(agent_turn_complete_event.wait(), timeout=30.0)
227
56
  except asyncio.TimeoutError:
228
57
  logger.error(f"Agent did not become idle within 30 seconds. Exiting.")
229
- # Gracefully exit if agent fails to start
230
58
  return
231
59
 
232
60
  if initial_prompt:
233
61
  logger.info(f"Initial prompt provided: '{initial_prompt}'")
234
- print(f"You: {initial_prompt}") # Mirroring user input is fine with print
62
+ print(f"You: {initial_prompt}")
235
63
  agent_turn_complete_event.clear()
236
- cli_manager.reset_turn_state()
64
+ cli_display.reset_turn_state()
237
65
  await agent.post_user_message(AgentInputUserMessage(content=initial_prompt))
238
66
  await agent_turn_complete_event.wait()
239
67
 
68
+ # Main input loop
240
69
  while True:
241
70
  agent_turn_complete_event.clear()
242
71
 
243
- if cli_manager.pending_approval_data:
244
- logger.debug("Waiting for tool approval from user...")
72
+ if cli_display.pending_approval_data:
245
73
  approval_input = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
246
74
  approval_input = approval_input.strip().lower()
247
75
 
248
- approval_data = cli_manager.pending_approval_data
249
- cli_manager.pending_approval_data = None
76
+ approval_data = cli_display.pending_approval_data
77
+ cli_display.pending_approval_data = None
250
78
 
251
79
  is_approved = approval_input in ["y", "yes"]
252
80
  reason = "User approved via CLI" if is_approved else "User denied via CLI"
253
-
254
- if is_approved:
255
- logger.info(f"User approved tool invocation '{approval_data.invocation_id}'.")
256
- else:
257
- logger.info(f"User denied tool invocation '{approval_data.invocation_id}'.")
258
81
 
259
82
  await agent.post_tool_execution_approval(approval_data.invocation_id, is_approved, reason)
260
83
 
@@ -265,35 +88,30 @@ async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool
265
88
  user_input = user_input.rstrip('\n')
266
89
 
267
90
  if user_input.lower().strip() in ["/quit", "/exit"]:
268
- logger.info("Exit command received.")
269
91
  break
270
92
  if not user_input.strip():
271
93
  continue
272
94
 
273
- logger.debug(f"User input received, posting to agent: '{user_input}'")
274
- cli_manager.reset_turn_state()
95
+ cli_display.reset_turn_state()
275
96
  await agent.post_user_message(AgentInputUserMessage(content=user_input))
276
97
 
277
98
  await agent_turn_complete_event.wait()
278
- logger.debug("Agent turn complete, looping.")
279
99
 
280
100
  except (KeyboardInterrupt, EOFError):
281
- logger.info("Exit signal received. Shutting down CLI.")
101
+ logger.info("Exit signal received.")
282
102
  except Exception as e:
283
103
  logger.error(f"An unexpected error occurred in the CLI main loop: {e}", exc_info=True)
284
104
  finally:
285
- logger.info("Cleaning up and shutting down interactive session...")
105
+ logger.info("Shutting down interactive session...")
286
106
  if not event_task.done():
287
107
  event_task.cancel()
288
108
  try:
289
109
  await event_task
290
110
  except asyncio.CancelledError:
291
- pass # This is expected
111
+ pass
292
112
 
293
113
  if agent.is_running:
294
- logger.info("Stopping agent...")
295
114
  await agent.stop()
296
115
 
297
- logger.info("Closing event stream...")
298
116
  await streamer.close()
299
117
  logger.info("Interactive session finished.")
@@ -0,0 +1,205 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ from typing import Optional, List, Dict, Any
5
+ import json
6
+
7
+ from autobyteus.agent.context.phases import AgentOperationalPhase
8
+ from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
9
+ from autobyteus.agent.streaming.stream_event_payloads import (
10
+ AssistantChunkData,
11
+ AssistantCompleteResponseData,
12
+ ToolInvocationApprovalRequestedData,
13
+ ToolInteractionLogEntryData,
14
+ AgentOperationalPhaseTransitionData,
15
+ ErrorEventData,
16
+ ToolInvocationAutoExecutingData,
17
+ )
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ class InteractiveCLIDisplay:
22
+ """
23
+ Manages the state and rendering logic for the interactive CLI session's display.
24
+ Input reading is handled by the main `run` loop. This class only handles output.
25
+ """
26
+ def __init__(self, agent_turn_complete_event: asyncio.Event, show_tool_logs: bool, show_token_usage: bool):
27
+ self.agent_turn_complete_event = agent_turn_complete_event
28
+ self.show_tool_logs = show_tool_logs
29
+ self.show_token_usage = show_token_usage
30
+ self.current_line_empty = True
31
+ self.agent_has_spoken_this_turn = False
32
+ self.pending_approval_data: Optional[ToolInvocationApprovalRequestedData] = None
33
+ self.is_thinking = False
34
+ self.is_in_content_block = False
35
+
36
+ def reset_turn_state(self):
37
+ """Resets flags that are tracked on a per-turn basis."""
38
+ self._end_thinking_block()
39
+ self.agent_has_spoken_this_turn = False
40
+ self.is_in_content_block = False
41
+
42
+ def _ensure_new_line(self):
43
+ """Ensures the cursor is on a new line if the current one isn't empty."""
44
+ if not self.current_line_empty:
45
+ sys.stdout.write("\n")
46
+ sys.stdout.flush()
47
+ self.current_line_empty = True
48
+
49
+ def _end_thinking_block(self):
50
+ """Closes the <Thinking> block if it was active."""
51
+ if self.is_thinking:
52
+ sys.stdout.write("\n</Thinking>")
53
+ sys.stdout.flush()
54
+ self.is_thinking = False
55
+ self.current_line_empty = False
56
+
57
+ def _display_tool_approval_prompt(self):
58
+ """Displays the tool approval prompt using stored pending data."""
59
+ if not self.pending_approval_data:
60
+ return
61
+
62
+ try:
63
+ args_str = json.dumps(self.pending_approval_data.arguments, indent=2)
64
+ except TypeError:
65
+ args_str = str(self.pending_approval_data.arguments)
66
+
67
+ self._ensure_new_line()
68
+ prompt_message = (
69
+ f"Tool Call: '{self.pending_approval_data.tool_name}' requests permission to run with arguments:\n"
70
+ f"{args_str}\nApprove? (y/n): "
71
+ )
72
+ sys.stdout.write(prompt_message)
73
+ sys.stdout.flush()
74
+ self.current_line_empty = False
75
+
76
+ async def handle_stream_event(self, event: StreamEvent):
77
+ """Processes a single StreamEvent and updates the CLI display."""
78
+ # A block of thinking ends if any event other than a reasoning chunk arrives.
79
+ is_reasoning_only_chunk = (
80
+ event.event_type == StreamEventType.ASSISTANT_CHUNK and
81
+ isinstance(event.data, AssistantChunkData) and
82
+ bool(event.data.reasoning) and not bool(event.data.content)
83
+ )
84
+ if not is_reasoning_only_chunk:
85
+ self._end_thinking_block()
86
+
87
+ # Most events should start on a new line.
88
+ if event.event_type != StreamEventType.ASSISTANT_CHUNK:
89
+ self._ensure_new_line()
90
+
91
+ if event.event_type in [
92
+ StreamEventType.AGENT_IDLE,
93
+ StreamEventType.ERROR_EVENT,
94
+ StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED,
95
+ ]:
96
+ self.agent_turn_complete_event.set()
97
+
98
+ if event.event_type == StreamEventType.ASSISTANT_CHUNK and isinstance(event.data, AssistantChunkData):
99
+ # If this is the first output from the agent this turn, print the "Agent: " prefix.
100
+ if not self.agent_has_spoken_this_turn:
101
+ self._ensure_new_line()
102
+ sys.stdout.write("Agent:\n")
103
+ sys.stdout.flush()
104
+ self.agent_has_spoken_this_turn = True
105
+ self.current_line_empty = True
106
+
107
+ # Stream reasoning to stdout without logger formatting.
108
+ if event.data.reasoning:
109
+ if not self.is_thinking:
110
+ sys.stdout.write("<Thinking>\n")
111
+ sys.stdout.flush()
112
+ self.is_thinking = True
113
+ self.current_line_empty = True # We just printed a newline
114
+
115
+ sys.stdout.write(event.data.reasoning)
116
+ sys.stdout.flush()
117
+ self.current_line_empty = event.data.reasoning.endswith('\n')
118
+
119
+ # Stream content to stdout.
120
+ if event.data.content:
121
+ if not self.is_in_content_block:
122
+ self._ensure_new_line() # Ensures content starts on a new line after </Thinking>
123
+ self.is_in_content_block = True
124
+ sys.stdout.write(event.data.content)
125
+ sys.stdout.flush()
126
+ self.current_line_empty = event.data.content.endswith('\n')
127
+
128
+ if self.show_token_usage and event.data.is_complete and event.data.usage:
129
+ self._ensure_new_line()
130
+ usage = event.data.usage
131
+ logger.info(
132
+ f"[Token Usage: Prompt={usage.prompt_tokens}, "
133
+ f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
134
+ )
135
+
136
+ elif event.event_type == StreamEventType.ASSISTANT_COMPLETE_RESPONSE and isinstance(event.data, AssistantCompleteResponseData):
137
+ # The reasoning has already been streamed. Do not log it again.
138
+
139
+ if not self.agent_has_spoken_this_turn:
140
+ # This case handles responses that might not have streamed any content chunks (e.g., only a tool call).
141
+ # We still need to ensure the agent's turn is visibly terminated with a newline.
142
+ self._ensure_new_line()
143
+
144
+ # If there's final content that wasn't in a chunk, print it.
145
+ if event.data.content:
146
+ sys.stdout.write(f"Agent: {event.data.content}\n")
147
+ sys.stdout.flush()
148
+
149
+ if self.show_token_usage and event.data.usage:
150
+ self._ensure_new_line()
151
+ usage = event.data.usage
152
+ logger.info(
153
+ f"[Token Usage: Prompt={usage.prompt_tokens}, "
154
+ f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
155
+ )
156
+
157
+ self.current_line_empty = True
158
+ self.reset_turn_state() # Reset for next turn
159
+
160
+ elif event.event_type == StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED and isinstance(event.data, ToolInvocationApprovalRequestedData):
161
+ self.pending_approval_data = event.data
162
+ self._display_tool_approval_prompt()
163
+
164
+ elif event.event_type == StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING and isinstance(event.data, ToolInvocationAutoExecutingData):
165
+ tool_name = event.data.tool_name
166
+ self._ensure_new_line()
167
+ sys.stdout.write(f"Agent: Automatically executing tool '{tool_name}'...\n")
168
+ sys.stdout.flush()
169
+ self.current_line_empty = True
170
+ self.agent_has_spoken_this_turn = True
171
+
172
+ elif event.event_type == StreamEventType.TOOL_INTERACTION_LOG_ENTRY and isinstance(event.data, ToolInteractionLogEntryData):
173
+ if self.show_tool_logs:
174
+ logger.info(
175
+ f"[Tool Log ({event.data.tool_name} | {event.data.tool_invocation_id})]: {event.data.log_entry}"
176
+ )
177
+
178
+ elif event.event_type == StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION and isinstance(event.data, AgentOperationalPhaseTransitionData):
179
+ if event.data.new_phase == AgentOperationalPhase.EXECUTING_TOOL:
180
+ tool_name = event.data.tool_name or "a tool"
181
+ sys.stdout.write(f"Agent: Waiting for tool '{tool_name}' to complete...\n")
182
+ sys.stdout.flush()
183
+ self.current_line_empty = True
184
+ self.agent_has_spoken_this_turn = True
185
+ elif event.data.new_phase == AgentOperationalPhase.BOOTSTRAPPING:
186
+ logger.info("[Agent is initializing...]")
187
+ elif event.data.new_phase == AgentOperationalPhase.TOOL_DENIED:
188
+ tool_name = event.data.tool_name or "a tool"
189
+ logger.info(f"[Tool '{tool_name}' was denied by user. Agent is reconsidering.]")
190
+ else:
191
+ phase_msg = f"[Agent Status: {event.data.new_phase.value}"
192
+ if event.data.tool_name:
193
+ phase_msg += f" ({event.data.tool_name})"
194
+ phase_msg += "]"
195
+ logger.info(phase_msg)
196
+
197
+ elif event.event_type == StreamEventType.ERROR_EVENT and isinstance(event.data, ErrorEventData):
198
+ logger.error(f"[Error: {event.data.message} (Source: {event.data.source})]")
199
+
200
+ elif event.event_type == StreamEventType.AGENT_IDLE:
201
+ logger.info("[Agent is now idle.]")
202
+
203
+ else:
204
+ # Add logging for unhandled events for better debugging
205
+ logger.debug(f"CLI Display: Unhandled StreamEvent type: {event.event_type.value}")
@@ -122,7 +122,8 @@ class EventManager(metaclass=SingletonMeta):
122
122
  listener(**final_args_to_pass)
123
123
 
124
124
  def emit(self, event_type: EventType, origin_object_id: Optional[str] = None, **kwargs: Any):
125
- available_kwargs_for_listeners = {"object_id": origin_object_id, **kwargs}
125
+ # FIX: Added 'event_type' to the dictionary passed to listeners.
126
+ available_kwargs_for_listeners = {"event_type": event_type, "object_id": origin_object_id, **kwargs}
126
127
 
127
128
  targeted_topic = Topic(event_type, origin_object_id)
128
129
  global_topic = Topic(event_type, None)
@@ -21,6 +21,7 @@ class EventType(Enum):
21
21
  AGENT_PHASE_AWAITING_LLM_RESPONSE_STARTED = "agent_phase_awaiting_llm_response_started"
22
22
  AGENT_PHASE_ANALYZING_LLM_RESPONSE_STARTED = "agent_phase_analyzing_llm_response_started"
23
23
  AGENT_PHASE_AWAITING_TOOL_APPROVAL_STARTED = "agent_phase_awaiting_tool_approval_started"
24
+ AGENT_PHASE_TOOL_DENIED_STARTED = "agent_phase_tool_denied_started"
24
25
  AGENT_PHASE_EXECUTING_TOOL_STARTED = "agent_phase_executing_tool_started"
25
26
  AGENT_PHASE_PROCESSING_TOOL_RESULT_STARTED = "agent_phase_processing_tool_result_started"
26
27
  AGENT_PHASE_SHUTTING_DOWN_STARTED = "agent_phase_shutting_down_started"
@@ -30,12 +31,13 @@ class EventType(Enum):
30
31
  # --- Agent Data Outputs ---
31
32
  AGENT_DATA_ASSISTANT_CHUNK = "agent_data_assistant_chunk"
32
33
  AGENT_DATA_ASSISTANT_CHUNK_STREAM_END = "agent_data_assistant_chunk_stream_end"
33
- AGENT_DATA_ASSISTANT_COMPLETE_RESPONSE = "agent_data_assistant_complete_response" # RENAMED from AGENT_DATA_ASSISTANT_FINAL_MESSAGE
34
+ AGENT_DATA_ASSISTANT_COMPLETE_RESPONSE = "agent_data_assistant_complete_response"
34
35
  AGENT_DATA_TOOL_LOG = "agent_data_tool_log"
35
36
  AGENT_DATA_TOOL_LOG_STREAM_END = "agent_data_tool_log_stream_end"
36
37
 
37
38
  # --- Agent Requests for External Interaction ---
38
39
  AGENT_REQUEST_TOOL_INVOCATION_APPROVAL = "agent_request_tool_invocation_approval"
40
+ AGENT_TOOL_INVOCATION_AUTO_EXECUTING = "agent_tool_invocation_auto_executing"
39
41
 
40
42
  # --- Agent Errors (not necessarily phase changes, e.g., error during output generation) ---
41
43
  AGENT_ERROR_OUTPUT_GENERATION = "agent_error_output_generation"
@@ -30,18 +30,13 @@ class AutobyteusLLM(BaseLLM):
30
30
  image_urls: Optional[List[str]] = None,
31
31
  **kwargs
32
32
  ) -> CompleteResponse:
33
- user_message_index = kwargs.get("user_message_index")
34
- if user_message_index is None:
35
- raise ValueError("user_message_index is required in kwargs")
36
-
37
33
  self.add_user_message(user_message)
38
34
  try:
39
35
  response = await self.client.send_message(
40
36
  conversation_id=self.conversation_id,
41
37
  model_name=self.model.name,
42
38
  user_message=user_message,
43
- file_paths=image_urls,
44
- user_message_index=user_message_index
39
+ image_urls=image_urls
45
40
  )
46
41
 
47
42
  assistant_message = response['response']
@@ -69,10 +64,6 @@ class AutobyteusLLM(BaseLLM):
69
64
  image_urls: Optional[List[str]] = None,
70
65
  **kwargs
71
66
  ) -> AsyncGenerator[ChunkResponse, None]:
72
- user_message_index = kwargs.get("user_message_index")
73
- if user_message_index is None:
74
- raise ValueError("user_message_index is required in kwargs")
75
-
76
67
  self.add_user_message(user_message)
77
68
  complete_response = ""
78
69
 
@@ -81,8 +72,7 @@ class AutobyteusLLM(BaseLLM):
81
72
  conversation_id=self.conversation_id,
82
73
  model_name=self.model.name,
83
74
  user_message=user_message,
84
- file_paths=image_urls,
85
- user_message_index=user_message_index
75
+ image_urls=image_urls
86
76
  ):
87
77
  if 'error' in chunk:
88
78
  raise RuntimeError(chunk['error'])