1111import uuid
1212import models
1313
14- from python .helpers import extract_tools , files , errors , history , tokens , context as context_helper
15- from python .helpers import dirty_json
14+ from python .helpers import (
15+ extract_tools ,
16+ files ,
17+ errors ,
18+ history ,
19+ tokens ,
20+ context as context_helper ,
21+ dirty_json ,
22+ subagents
23+ )
1624from python .helpers .print_style import PrintStyle
1725
1826from langchain_core .prompts import (
@@ -69,9 +77,10 @@ def __init__(
6977 # initialize state
7078 self .name = name
7179 self .config = config
80+ self .data = data or {}
81+ self .output_data = output_data or {}
7282 self .log = log or Log .Log ()
7383 self .log .context = self
74- self .agent0 = agent0 or Agent (0 , self .config , self )
7584 self .paused = paused
7685 self .streaming_agent = streaming_agent
7786 self .task : DeferredTask | None = None
@@ -80,10 +89,9 @@ def __init__(
8089 AgentContext ._counter += 1
8190 self .no = AgentContext ._counter
8291 self .last_message = last_message or datetime .now (timezone .utc )
83- self .data = data or {}
84- self .output_data = output_data or {}
85-
8692
93+ # initialize agent at last (context is complete now)
94+ self .agent0 = agent0 or Agent (0 , self .config , self )
8795
8896 @staticmethod
8997 def get (id : str ):
@@ -100,7 +108,7 @@ def use(id: str):
100108
101109 @staticmethod
102110 def current ():
103- ctxid = context_helper .get_context_data ("agent_context_id" ,"" )
111+ ctxid = context_helper .get_context_data ("agent_context_id" , "" )
104112 if not ctxid :
105113 return None
106114 return AgentContext .get (ctxid )
@@ -122,7 +130,8 @@ def all():
122130 @staticmethod
123131 def generate_id ():
124132 def generate_short_id ():
125- return '' .join (random .choices (string .ascii_letters + string .digits , k = 8 ))
133+ return "" .join (random .choices (string .ascii_letters + string .digits , k = 8 ))
134+
126135 while True :
127136 short_id = generate_short_id ()
128137 if short_id not in AgentContext ._contexts :
@@ -132,6 +141,7 @@ def generate_short_id():
132141 def get_notification_manager (cls ):
133142 if cls ._notification_manager is None :
134143 from python .helpers .notification import NotificationManager # type: ignore
144+
135145 cls ._notification_manager = NotificationManager ()
136146 return cls ._notification_manager
137147
@@ -269,7 +279,6 @@ async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True
269279 agent .handle_critical_exception (e )
270280
271281
272-
273282@dataclass
274283class AgentConfig :
275284 chat_model : models .ModelConfig
@@ -280,7 +289,9 @@ class AgentConfig:
280289 profile : str = ""
281290 memory_subdir : str = ""
282291 knowledge_subdirs : list [str ] = field (default_factory = lambda : ["default" , "custom" ])
283- browser_http_headers : dict [str , str ] = field (default_factory = dict ) # Custom HTTP headers for browser requests
292+ browser_http_headers : dict [str , str ] = field (
293+ default_factory = dict
294+ ) # Custom HTTP headers for browser requests
284295 code_exec_ssh_enabled : bool = True
285296 code_exec_ssh_addr : str = "localhost"
286297 code_exec_ssh_port : int = 55022
@@ -354,6 +365,7 @@ def __init__(
354365 asyncio .run (self .call_extensions ("agent_init" ))
355366
356367 async def monologue (self ):
368+ error_retries = 0 # counter for critical error retries
357369 while True :
358370 try :
359371 # loop data dictionary to pass to extensions
@@ -380,7 +392,9 @@ async def monologue(self):
380392 prompt = await self .prepare_prompt (loop_data = self .loop_data )
381393
382394 # call before_main_llm_call extensions
383- await self .call_extensions ("before_main_llm_call" , loop_data = self .loop_data )
395+ await self .call_extensions (
396+ "before_main_llm_call" , loop_data = self .loop_data
397+ )
384398
385399 async def reasoning_callback (chunk : str , full : str ):
386400 await self .handle_intervention ()
@@ -389,7 +403,9 @@ async def reasoning_callback(chunk: str, full: str):
389403 # Pass chunk and full data to extensions for processing
390404 stream_data = {"chunk" : chunk , "full" : full }
391405 await self .call_extensions (
392- "reasoning_stream_chunk" , loop_data = self .loop_data , stream_data = stream_data
406+ "reasoning_stream_chunk" ,
407+ loop_data = self .loop_data ,
408+ stream_data = stream_data ,
393409 )
394410 # Stream masked chunk after extensions processed it
395411 if stream_data .get ("chunk" ):
@@ -405,7 +421,9 @@ async def stream_callback(chunk: str, full: str):
405421 # Pass chunk and full data to extensions for processing
406422 stream_data = {"chunk" : chunk , "full" : full }
407423 await self .call_extensions (
408- "response_stream_chunk" , loop_data = self .loop_data , stream_data = stream_data
424+ "response_stream_chunk" ,
425+ loop_data = self .loop_data ,
426+ stream_data = stream_data ,
409427 )
410428 # Stream masked chunk after extensions processed it
411429 if stream_data .get ("chunk" ):
@@ -453,6 +471,7 @@ async def stream_callback(chunk: str, full: str):
453471
454472 # exceptions inside message loop:
455473 except InterventionException as e :
474+ error_retries = 0 # reset retry counter on user intervention
456475 pass # intervention message has been handled in handle_intervention(), proceed with conversation loop
457476 except RepairableException as e :
458477 # Forward repairable errors to the LLM, maybe it can fix them
@@ -462,8 +481,10 @@ async def stream_callback(chunk: str, full: str):
462481 PrintStyle (font_color = "red" , padding = True ).print (msg ["message" ])
463482 self .context .log .log (type = "error" , content = msg ["message" ])
464483 except Exception as e :
465- # Other exception kill the loop
466- self .handle_critical_exception (e )
484+ # Retry critical exceptions before failing
485+ error_retries = await self .retry_critical_exception (
486+ e , error_retries
487+ )
467488
468489 finally :
469490 # call message_loop_end extensions
@@ -473,9 +494,13 @@ async def stream_callback(chunk: str, full: str):
473494
474495 # exceptions outside message loop:
475496 except InterventionException as e :
497+ error_retries = 0 # reset retry counter on user intervention
476498 pass # just start over
477499 except Exception as e :
478- self .handle_critical_exception (e )
500+ # Retry critical exceptions before failing
501+ error_retries = await self .retry_critical_exception (
502+ e , error_retries
503+ )
479504 finally :
480505 self .context .streaming_agent = None # unset current streamer
481506 # call monologue_end extensions
@@ -532,6 +557,30 @@ async def prepare_prompt(self, loop_data: LoopData) -> list[BaseMessage]:
532557
533558 return full_prompt
534559
560+ async def retry_critical_exception (
561+ self , e : Exception , error_retries : int , delay : int = 3 , max_retries : int = 1
562+ ) -> int :
563+ if error_retries >= max_retries :
564+ self .handle_critical_exception (e )
565+
566+ error_message = errors .format_error (e )
567+
568+ self .context .log .log (
569+ type = "warning" , content = "Critical error occurred, retrying..."
570+ )
571+ PrintStyle (font_color = "orange" , padding = True ).print (
572+ "Critical error occurred, retrying..."
573+ )
574+ await asyncio .sleep (delay )
575+ agent_facing_error = self .read_prompt (
576+ "fw.msg_critical_error.md" , error_message = error_message
577+ )
578+ self .hist_add_warning (message = agent_facing_error )
579+ PrintStyle (font_color = "orange" , padding = True ).print (
580+ agent_facing_error
581+ )
582+ return error_retries + 1
583+
535584 def handle_critical_exception (self , exception : Exception ):
536585 if isinstance (exception , HandledException ):
537586 raise exception # Re-raise the exception to kill the loop
@@ -570,27 +619,15 @@ async def get_system_prompt(self, loop_data: LoopData) -> list[str]:
570619 return system_prompt
571620
572621 def parse_prompt (self , _prompt_file : str , ** kwargs ):
573- dirs = [files .get_abs_path ("prompts" )]
574- if (
575- self .config .profile
576- ): # if agent has custom folder, use it and use default as backup
577- prompt_dir = files .get_abs_path ("agents" , self .config .profile , "prompts" )
578- dirs .insert (0 , prompt_dir )
622+ dirs = subagents .get_paths (self , "prompts" )
579623 prompt = files .parse_file (
580- _prompt_file , _directories = dirs , ** kwargs
624+ _prompt_file , _directories = dirs , _agent = self , ** kwargs
581625 )
582626 return prompt
583627
584628 def read_prompt (self , file : str , ** kwargs ) -> str :
585- dirs = [files .get_abs_path ("prompts" )]
586- if (
587- self .config .profile
588- ): # if agent has custom folder, use it and use default as backup
589- prompt_dir = files .get_abs_path ("agents" , self .config .profile , "prompts" )
590- dirs .insert (0 , prompt_dir )
591- prompt = files .read_prompt_file (
592- file , _directories = dirs , ** kwargs
593- )
629+ dirs = subagents .get_paths (self , "prompts" )
630+ prompt = files .read_prompt_file (file , _directories = dirs , _agent = self , ** kwargs )
594631 prompt = files .remove_code_fences (prompt )
595632 return prompt
596633
@@ -606,8 +643,12 @@ def hist_add_message(
606643 self .last_message = datetime .now (timezone .utc )
607644 # Allow extensions to process content before adding to history
608645 content_data = {"content" : content }
609- asyncio .run (self .call_extensions ("hist_add_before" , content_data = content_data , ai = ai ))
610- return self .history .add_message (ai = ai , content = content_data ["content" ], tokens = tokens )
646+ asyncio .run (
647+ self .call_extensions ("hist_add_before" , content_data = content_data , ai = ai )
648+ )
649+ return self .history .add_message (
650+ ai = ai , content = content_data ["content" ], tokens = tokens
651+ )
611652
612653 def hist_add_user_message (self , message : UserMessage , intervention : bool = False ):
613654 self .history .new_topic () # user message starts a new topic in history
@@ -720,7 +761,9 @@ async def stream_callback(chunk: str, total: str):
720761 system_message = call_data ["system" ],
721762 user_message = call_data ["message" ],
722763 response_callback = stream_callback if call_data ["callback" ] else None ,
723- rate_limiter_callback = self .rate_limiter_callback if not call_data ["background" ] else None ,
764+ rate_limiter_callback = (
765+ self .rate_limiter_callback if not call_data ["background" ] else None
766+ ),
724767 )
725768
726769 return response
@@ -742,7 +785,9 @@ async def call_chat_model(
742785 messages = messages ,
743786 reasoning_callback = reasoning_callback ,
744787 response_callback = response_callback ,
745- rate_limiter_callback = self .rate_limiter_callback if not background else None ,
788+ rate_limiter_callback = (
789+ self .rate_limiter_callback if not background else None
790+ ),
746791 )
747792
748793 return response , reasoning
@@ -817,11 +862,15 @@ async def process_tools(self, msg: str):
817862 # Fallback to local get_tool if MCP tool was not found or MCP lookup failed
818863 if not tool :
819864 tool = self .get_tool (
820- name = tool_name , method = tool_method , args = tool_args , message = msg , loop_data = self .loop_data
865+ name = tool_name ,
866+ method = tool_method ,
867+ args = tool_args ,
868+ message = msg ,
869+ loop_data = self .loop_data ,
821870 )
822871
823872 if tool :
824- self .loop_data .current_tool = tool # type: ignore
873+ self .loop_data .current_tool = tool # type: ignore
825874 try :
826875 await self .handle_intervention ()
827876
@@ -830,14 +879,20 @@ async def process_tools(self, msg: str):
830879 await self .handle_intervention ()
831880
832881 # Allow extensions to preprocess tool arguments
833- await self .call_extensions ("tool_execute_before" , tool_args = tool_args or {}, tool_name = tool_name )
882+ await self .call_extensions (
883+ "tool_execute_before" ,
884+ tool_args = tool_args or {},
885+ tool_name = tool_name ,
886+ )
834887
835888 response = await tool .execute (** tool_args )
836889 await self .handle_intervention ()
837890
838891 # Allow extensions to postprocess tool response
839- await self .call_extensions ("tool_execute_after" , response = response , tool_name = tool_name )
840-
892+ await self .call_extensions (
893+ "tool_execute_after" , response = response , tool_name = tool_name
894+ )
895+
841896 await tool .after_execution (response )
842897 await self .handle_intervention ()
843898
@@ -889,34 +944,40 @@ async def handle_response_stream(self, stream: str):
889944 pass
890945
891946 def get_tool (
892- self , name : str , method : str | None , args : dict , message : str , loop_data : LoopData | None , ** kwargs
947+ self ,
948+ name : str ,
949+ method : str | None ,
950+ args : dict ,
951+ message : str ,
952+ loop_data : LoopData | None ,
953+ ** kwargs ,
893954 ):
894955 from python .tools .unknown import Unknown
895956 from python .helpers .tool import Tool
896957
897958 classes = []
898959
899- # try agent tools first
900- if self .config .profile :
960+ # search for tools in agent's folder hierarchy
961+ paths = subagents .get_paths (self , "tools" , name + ".py" , default_root = "python" )
962+ for path in paths :
901963 try :
902- classes = extract_tools .load_classes_from_file (
903- "agents/" + self .config .profile + "/tools/" + name + ".py" , Tool # type: ignore[arg-type]
904- )
964+ classes = extract_tools .load_classes_from_file (path , Tool ) # type: ignore[arg-type]
965+ break
905966 except Exception :
906- pass
967+ continue
907968
908- # try default tools
909- if not classes :
910- try :
911- classes = extract_tools .load_classes_from_file (
912- "python/tools/" + name + ".py" , Tool # type: ignore[arg-type]
913- )
914- except Exception as e :
915- pass
916969 tool_class = classes [0 ] if classes else Unknown
917970 return tool_class (
918- agent = self , name = name , method = method , args = args , message = message , loop_data = loop_data , ** kwargs
971+ agent = self ,
972+ name = name ,
973+ method = method ,
974+ args = args ,
975+ message = message ,
976+ loop_data = loop_data ,
977+ ** kwargs ,
919978 )
920979
921980 async def call_extensions (self , extension_point : str , ** kwargs ) -> Any :
922- return await call_extensions (extension_point = extension_point , agent = self , ** kwargs )
981+ return await call_extensions (
982+ extension_point = extension_point , agent = self , ** kwargs
983+ )
0 commit comments