Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 72 additions & 41 deletions agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,43 +592,49 @@ async def prepare_prompt(self, loop_data: LoopData) -> list[BaseMessage]:

@extension.extensible
async def handle_exception(self, location: str, exception: Exception):
if exception:
raise exception # exception handling is done by extensions
if not exception:
return

if isinstance(exception, errors.HandledException):
raise exception
elif isinstance(exception, asyncio.CancelledError):
PrintStyle(font_color="white", background_color="red", padding=True).print(
f"Context {self.context.id} terminated during message loop"
)
raise errors.HandledException(exception)
elif isinstance(exception, errors.InterventionException):
# Intervention exceptions just skip the rest of the message loop iteration
return
elif isinstance(exception, errors.RepairableException):
# Repairable exceptions should be caught and added to chat history
# so the model can fix its mistake.
error_msg = str(exception)
PrintStyle(font_color="yellow", padding=True).print(f"Repairable Error: {error_msg}")

# Create a log entry and warning message
wmsg = self.hist_add_warning(error_msg)
self.context.log.log(
type="warning",
content=f"{self.agent_name}: {error_msg}",
id=wmsg.id
)
return

else:
error_text = errors.error_text(exception)
error_message = errors.format_error(exception)

# exception_data = {"exception": exception}
# await self.call_extensions(
# "message_loop_exception", exception_data=exception_data
# )

# # If extensions cleared the exception, continue.
# if not exception_data.get("exception"):
# return

# # Backwards-compatible fallback (should normally be handled by _90 extension).
# exception = exception_data["exception"]
# if isinstance(exception, HandledException):
# raise exception
# elif isinstance(exception, asyncio.CancelledError):
# PrintStyle(font_color="white", background_color="red", padding=True).print(
# f"Context {self.context.id} terminated during message loop"
# )
# raise HandledException(exception)

# else:
# error_text = errors.error_text(exception)
# error_message = errors.format_error(exception)

# # Mask secrets in error messages
# PrintStyle(font_color="red", padding=True).print(error_message)
# self.context.log.log(
# type="error",
# content=error_message,
# )
# PrintStyle(font_color="red", padding=True).print(
# f"{self.agent_name}: {error_text}"
# )

# raise HandledException(exception) # Re-raise the exception to kill the loop
# Mask secrets in error messages
PrintStyle(font_color="red", padding=True).print(error_message)
self.context.log.log(
type="error",
content=error_message,
)
PrintStyle(font_color="red", padding=True).print(
f"{self.agent_name}: {error_text}"
)

raise errors.HandledException(exception) # Re-raise the exception to kill the loop

@extension.extensible
async def get_system_prompt(self, loop_data: LoopData) -> list[str]:
Expand Down Expand Up @@ -975,11 +981,36 @@ async def process_tools(self, msg: str):
@extension.extensible
async def validate_tool_request(self, tool_request: Any):
if not isinstance(tool_request, dict):
raise ValueError("Tool request must be a dictionary")
if not tool_request.get("tool_name") or not isinstance(tool_request.get("tool_name"), str):
raise ValueError("Tool request must have a tool_name (type string) field")
if not tool_request.get("tool_args") or not isinstance(tool_request.get("tool_args"), dict):
raise ValueError("Tool request must have a tool_args (type dictionary) field")
received_type = type(tool_request).__name__
raise RepairableException(
f"Tool request must be a dictionary (JSON object). Received: {received_type}. "
"Please ensure your tool call is wrapped in a valid JSON object. "
"Example: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}}"
)

# Support aliases 'tool_name' and 'tool'
tool_name = tool_request.get("tool_name", tool_request.get("tool"))
if not tool_name or not isinstance(tool_name, str):
received_keys = list(tool_request.keys())
raise RepairableException(
f"Tool request is missing a 'tool_name' or 'tool' string field. "
"Both are supported, but one is required. "
f"Received keys: {received_keys}. "
"Correct format: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}} "
"OR {'tool': 'python', 'args': {'code': 'print(1)'}}"
)

# Support aliases 'tool_args' and 'args'
tool_args = tool_request.get("tool_args", tool_request.get("args"))
# Allow missing args (None) or empty dict, but if present must be a dict
if tool_args is not None and not isinstance(tool_args, dict):
received_type = type(tool_args).__name__
raise RepairableException(
f"The 'tool_args' (or 'args') field must be a dictionary of parameters. "
f"Received: {received_type}. "
"Correct format: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}} "
"OR {'tool': 'python', 'args': {'code': 'print(1)'}}"
)



Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from helpers.extension import Extension
import asyncio
from agent import LoopData
from plugins._memory.extensions.python.message_loop_prompts_after._50_recall_memories import DATA_NAME_TASK as DATA_NAME_TASK_MEMORIES, DATA_NAME_ITER as DATA_NAME_ITER_MEMORIES
from helpers import plugins
Expand Down Expand Up @@ -27,4 +28,11 @@ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
return

# otherwise await the task
await task
try:
await task
except TimeoutError:
self.agent.context.log.log(type="warning", heading="Memory recall timed out after 30 seconds.")
except asyncio.CancelledError:
pass
except Exception as e:
self.agent.context.log.log(type="warning", heading=f"Memory recall failed: {e}")