@@ -285,6 +285,12 @@ def set_requesting_help(self, requesting_help: bool):
285285
286286class ABIAgentState (MessagesState ):
287287 system_prompt : str
288+ # Routing state persisted through the LangGraph checkpointer so that agent
289+ # delegation survives across separate HTTP turns (each turn rebuilds the
290+ # agent tree with a fresh AgentSharedState, so in-memory routing alone is
291+ # lost between requests).
292+ current_active_agent : Optional [str ]
293+ supervisor_agent : Optional [str ]
288294
289295
290296@dataclass
@@ -788,6 +794,19 @@ def current_active_agent(self, state: ABIAgentState) -> Command:
788794 Returns:
789795 Command: Command to goto the current active agent
790796 """
797+ # Restore routing from the checkpointed graph state. Within a single
798+ # long-lived instance ``self._state`` is already authoritative, so we
799+ # only hydrate when it is empty — which is exactly the case at the start
800+ # of a new HTTP turn, where the agent tree was freshly duplicated with a
801+ # blank AgentSharedState. This is what makes a prior handoff survive
802+ # across requests instead of falling back to the supervisor every turn.
803+ persisted_active = state .get ("current_active_agent" )
804+ if persisted_active is not None and self ._state .current_active_agent is None :
805+ self ._state .set_current_active_agent (persisted_active )
806+ persisted_supervisor = state .get ("supervisor_agent" )
807+ if persisted_supervisor is not None and self ._state .supervisor_agent is None :
808+ self ._state .set_supervisor_agent (persisted_supervisor )
809+
791810 # Log the current active agent
792811 logger .debug (f"😏 Supervisor agent: '{ self ._state .supervisor_agent } '" )
793812 logger .debug (f"🟢 Active agent: '{ self ._state .current_active_agent } '" )
@@ -824,7 +843,10 @@ def current_active_agent(self, state: ABIAgentState) -> Command:
824843 self ._notify_agent_routing (agent_name )
825844 return Command (
826845 goto = agent_name ,
827- update = {"messages" : state ["messages" ]},
846+ update = {
847+ "messages" : state ["messages" ],
848+ "current_active_agent" : agent_name ,
849+ },
828850 )
829851 else :
830852 logger .debug (f"❌ Agent '{ at_mention } ' not found" )
@@ -1034,6 +1056,16 @@ def call_model(
10341056 logger .debug (f"🧠 Calling model for agent '{ self ._name } '" )
10351057 self ._notify_call_model (self ._name )
10361058
1059+ # Persist routing into the checkpointed graph state so that the next
1060+ # HTTP turn (which rebuilds the agent tree from scratch) restores who is
1061+ # currently handling the conversation instead of resetting to the
1062+ # supervisor. A handoff downstream (see make_handoff_tool) overrides
1063+ # this value within the same step.
1064+ routing_update : dict [str , Any ] = {
1065+ "current_active_agent" : self ._state .current_active_agent ,
1066+ "supervisor_agent" : self ._state .supervisor_agent ,
1067+ }
1068+
10371069 # Inserting system prompt before messages.
10381070 messages = state ["messages" ]
10391071 if (
@@ -1056,11 +1088,12 @@ def call_model(
10561088 return Command (
10571089 goto = "__end__" ,
10581090 update = {
1091+ ** routing_update ,
10591092 "messages" : [
10601093 AIMessage (
10611094 content = f"I'm sorry, I encountered an error while processing your request:\n \n { e } "
10621095 )
1063- ]
1096+ ],
10641097 },
10651098 )
10661099 logger .debug (f"Model response: { response } " )
@@ -1079,12 +1112,18 @@ def call_model(
10791112 #### -----> A solution would be to rebuild the state to make sure that the following message of a tool call it the response of that call. If we do that we should theroetically be able to call multiple tools at once, which would be more effective.
10801113 # response.tool_calls = [response.tool_calls[0]]
10811114
1082- return Command (goto = "call_tools" , update = {"messages" : [response ]})
1115+ return Command (
1116+ goto = "call_tools" ,
1117+ update = {** routing_update , "messages" : [response ]},
1118+ )
10831119
10841120 if self ._markdown_pretty_display :
10851121 logger .debug ("Applying Markdown pretty display to response" )
10861122 response = self ._pretty_display_markdown (response )
1087- return Command (goto = "__end__" , update = {"messages" : [response ]})
1123+ return Command (
1124+ goto = "__end__" ,
1125+ update = {** routing_update , "messages" : [response ]},
1126+ )
10881127
10891128 def call_tools (self , state : ABIAgentState ) -> list [Command ]:
10901129 # Check if messages are present in the state.
@@ -1121,7 +1160,37 @@ def call_tools(self, state: ABIAgentState) -> list[Command]:
11211160 for tool_call in tool_calls :
11221161 tool_name : str = tool_call ["name" ]
11231162 logger .debug (f"🛠️ Calling tool: { tool_name } " )
1124- tool_ : BaseTool = self ._tools_by_name [tool_name ]
1163+
1164+ # Guard against hallucinated / out-of-scope tool names. A raw dict
1165+ # lookup here would raise KeyError and kill the whole invoke thread
1166+ # (the user sees a generic crash). Instead, surface a ToolMessage the
1167+ # model can read so it can self-correct on the next loop.
1168+ tool_ = self ._tools_by_name .get (tool_name )
1169+ if tool_ is None :
1170+ available = sorted (self ._tools_by_name .keys ())
1171+ logger .error (
1172+ f"🚨 Agent '{ self ._name } ' tried to call unknown tool "
1173+ f"'{ tool_name } '. Available tools: { available } "
1174+ )
1175+ had_tool_error = True
1176+ results .append (
1177+ Command (
1178+ update = {
1179+ "messages" : [
1180+ ToolMessage (
1181+ content = (
1182+ f"Tool '{ tool_name } ' is not available to "
1183+ f"agent '{ self ._name } '. Available tools: "
1184+ f"{ available } "
1185+ ),
1186+ name = tool_name ,
1187+ tool_call_id = tool_call ["id" ],
1188+ )
1189+ ]
1190+ },
1191+ )
1192+ )
1193+ continue
11251194
11261195 tool_input_fields = tool_ .get_input_schema ().model_json_schema ()[
11271196 "properties"
@@ -1881,7 +1950,13 @@ def handoff_to_agent(
18811950 # This is the state update that the agent `agent_name` will see when it is invoked.
18821951 # We're passing agent's FULL internal message history AND adding a tool message to make sure
18831952 # the resulting chat history is valid. See the paragraph above for more information.
1884- update = {"messages" : state ["messages" ] + [tool_message ]},
1953+ # ``current_active_agent`` is persisted through the checkpointer so the
1954+ # delegation is restored on the next turn instead of routing back to
1955+ # the supervisor.
1956+ update = {
1957+ "messages" : state ["messages" ] + [tool_message ],
1958+ "current_active_agent" : agent_name ,
1959+ },
18851960 )
18861961
18871962 assert isinstance (handoff_to_agent , BaseTool )
0 commit comments