@@ -126,6 +126,9 @@ def connect(self, channels: GatewayChannels) -> None:
126126 log .info (f"Fetching bot info for { backend } ..." )
127127 self ._fetch_bot_info (backend )
128128
129+ # Inject backends into AgentCommand subclasses
130+ self ._inject_backends_into_agent_commands ()
131+
129132 # Subscribe to messages from all adapters
130133 # chatom provides unified Message type across all backends
131134 messages_in = csp .null_ts (Message )
@@ -248,6 +251,53 @@ def _update_user_access_loop(self, backend: str) -> None:
248251 except Exception :
249252 log .exception (f"Error updating user access for { backend } " )
250253
254+ def _inject_backends_into_agent_commands (self ) -> None :
255+ """Inject connected BackendBase instances into AgentCommand subclasses."""
256+ try :
257+ from csp_bot .commands .agent import AgentCommand
258+ except ImportError :
259+ # pydantic-ai / chatom[agent] not installed — skip silently
260+ return
261+
262+ backends = {}
263+ loops = {}
264+ for name in self ._adapters :
265+ result = self ._ensure_backend_connected (name )
266+ if result :
267+ connected_backend , loop = result
268+ backends [name ] = connected_backend
269+ loops [name ] = loop
270+ if backends :
271+ AgentCommand .set_backends (backends , loops = loops )
272+ log .info (f"Injected { len (backends )} connected backends into AgentCommand: { list (backends .keys ())} " )
273+
274+ def _track_agent_session_response (self , response : Message , command : BotCommand ) -> None :
275+ """Associate a sent response with an agent session for reply tracking.
276+
277+ Uses the original command's message ID as the key that future replies
278+ will reference (e.g., thread_ts in Slack, or message reference in Discord).
279+ """
280+ metadata = response .metadata or {}
281+ session_key = metadata .get ("agent_session_key" )
282+ if not session_key :
283+ return
284+
285+ try :
286+ from csp_bot .commands .agent import AgentCommand
287+ except ImportError :
288+ return
289+
290+ # Use the original command's message ID — in Slack threads, replies
291+ # reference this as thread_ts; in Discord, as message_reference.
292+ orig_msg_id = command .message .id if command .message else None
293+ if orig_msg_id :
294+ AgentCommand ._sessions .update_response_id (session_key , orig_msg_id )
295+ log .debug (f"Tracked agent session response: session={ session_key } , msg_id={ orig_msg_id } " )
296+
297+ # Also track by the response message ID if it has one
298+ if response .id and response .id != orig_msg_id :
299+ AgentCommand ._sessions .update_response_id (session_key , response .id )
300+
251301 def _ensure_backend_connected (self , backend : str ) -> Optional [Tuple [Any , asyncio .AbstractEventLoop ]]:
252302 """Ensure a connected backend exists for the given platform.
253303
@@ -579,6 +629,8 @@ def _handle_commands(self, cmd: ts[BotCommand]) -> Outputs(messages=ts[[Message]
579629 if isinstance (item , Message ):
580630 log .debug (f"Adding message to buffer: { item .content [:100 ] if item .content else 'empty' } ..." )
581631 s_buffer .append (item )
632+ # Track agent session responses for reply continuity
633+ self ._track_agent_session_response (item , command )
582634 elif isinstance (item , BotCommand ):
583635 next_cycle_commands .append (item )
584636 else :
@@ -821,6 +873,11 @@ def _extract_commands(
821873
822874 # Check for command syntax (supports both / and ! prefixes)
823875 if not content .startswith ("/" ) and not content .startswith ("!" ):
876+ # Check if this is a reply to an active agent session
877+ session_cmd = self ._check_agent_session_reply (msg , backend , channel_id )
878+ if session_cmd :
879+ return session_cmd
880+
824881 # If tagged but no command, show help
825882 log .info ("No command prefix, showing help" )
826883 return self ._create_help_command (msg , backend , channel_id )
@@ -914,6 +971,79 @@ def _extract_commands(
914971 log .exception ("Error extracting command" )
915972 return None
916973
974+ def _check_agent_session_reply (self , msg : Message , backend : str , channel_id : str ) -> Optional [BotCommand ]:
975+ """Check if the message is a reply to a bot response with an active agent session.
976+
977+ If so, constructs a BotCommand to continue the conversation.
978+ """
979+ try :
980+ from csp_bot .commands .agent import AgentCommand
981+ except ImportError :
982+ return None
983+
984+ # Get the referenced message ID
985+ ref_id = None
986+ if msg .reference and msg .reference .message_id :
987+ ref_id = msg .reference .message_id
988+ elif msg .reply_to and msg .reply_to .id :
989+ ref_id = msg .reply_to .id
990+ # Check thread metadata (Slack thread_ts)
991+ if not ref_id and msg .thread and msg .thread .id :
992+ ref_id = msg .thread .id
993+
994+ if not ref_id :
995+ return None
996+
997+ # Look up session by the bot response ID
998+ session = AgentCommand ._sessions .get_by_response_id (ref_id )
999+ if session is None :
1000+ return None
1001+
1002+ # Found an active session — route the reply to the same command
1003+ command_name = session .command_name
1004+ if command_name not in self ._commands :
1005+ log .warning (f"Agent session references unknown command: { command_name } " )
1006+ return None
1007+
1008+ command_runner = self ._commands [command_name ]
1009+ content = msg .content or ""
1010+ # Strip bot mention if present
1011+ bot_name = self ._get_bot_name (backend )
1012+ bot_id = self ._get_bot_id (backend )
1013+ if bot_id :
1014+ content = re .sub (rf"<@!?{ re .escape (bot_id )} >" , "" , content ).strip ()
1015+ if bot_name and content .startswith (f"@{ bot_name } " ):
1016+ content = content [len (f"@{ bot_name } " ) :].strip ()
1017+
1018+ source = User (
1019+ id = msg .author .id if msg .author else msg .author_id or "" ,
1020+ name = msg .author .name if msg .author else "" ,
1021+ email = getattr (msg .author , "email" , "" ) if msg .author else "" ,
1022+ handle = getattr (msg .author , "handle" , "" ) if msg .author else "" ,
1023+ )
1024+
1025+ channel_name = ""
1026+ if msg .channel and hasattr (msg .channel , "name" ):
1027+ channel_name = msg .channel .name or ""
1028+
1029+ bot_cmd = BotCommand (
1030+ command = command_name ,
1031+ args = (content ,),
1032+ source = source ,
1033+ targets = (),
1034+ channel_id = channel_id ,
1035+ channel_name = channel_name ,
1036+ backend = backend ,
1037+ variant = command_runner .kind () if isinstance (command_runner , BaseCommand ) else CommandVariant .REPLY ,
1038+ message = msg ,
1039+ delay = None ,
1040+ schedule = "" ,
1041+ times_run = 0 ,
1042+ )
1043+
1044+ log .info (f"Routing reply to active agent session: command={ command_name } , user={ source .id } " )
1045+ return command_runner .preexecute (bot_cmd )
1046+
9171047 def _parse_command_args (
9181048 self ,
9191049 tokens : List [str ],
0 commit comments