44
55use anyhow:: { Context , Result , bail} ;
66use clap:: Parser ;
7+ use std:: collections:: HashSet ;
78use std:: path:: PathBuf ;
89
910use crate :: styled_output:: { print_info, print_success, print_warning} ;
@@ -14,6 +15,7 @@ use cortex_protocol::{
1415 ParsedCommand , UserMessageEvent ,
1516} ;
1617
18+ use crate :: agent_cmd:: load_all_agents;
1719use crate :: export_cmd:: { ExportMessage , SessionExport } ;
1820
1921/// Import a session from JSON format.
@@ -102,6 +104,25 @@ impl ImportCommand {
102104 ) ;
103105 }
104106
107+ // Validate agent references in the imported session
108+ let missing_agents = validate_agent_references ( & export) ?;
109+ if !missing_agents. is_empty ( ) {
110+ eprintln ! (
111+ "Warning: The following agent references in this session are not available locally:"
112+ ) ;
113+ for agent in & missing_agents {
114+ eprintln ! ( " - @{}" , agent) ;
115+ }
116+ eprintln ! ( ) ;
117+ eprintln ! (
118+ "The session will be imported, but agent-related functionality may not work as expected."
119+ ) ;
120+ eprintln ! (
121+ "To fix this, create the missing agents using 'cortex agent create <name>' or copy them from the source system."
122+ ) ;
123+ eprintln ! ( ) ;
124+ }
125+
105126 // Generate a new session ID (we always create a new session on import)
106127 let new_conversation_id = ConversationId :: new ( ) ;
107128
@@ -235,6 +256,54 @@ async fn fetch_url(url: &str) -> Result<String> {
235256 }
236257}
237258
259+ /// Validate agent references in the imported session.
260+ /// Returns a list of missing agent names that are referenced but not available locally.
261+ fn validate_agent_references ( export : & SessionExport ) -> Result < Vec < String > > {
262+ // Get all locally available agents
263+ let local_agents: HashSet < String > = match load_all_agents ( ) {
264+ Ok ( agents) => agents. into_iter ( ) . map ( |a| a. name ) . collect ( ) ,
265+ Err ( e) => {
266+ // If we can't load agents, log a warning but continue
267+ tracing:: warn!( "Could not load local agents for validation: {}" , e) ;
268+ HashSet :: new ( )
269+ }
270+ } ;
271+
272+ let mut missing_agents = Vec :: new ( ) ;
273+
274+ // Check the session's agent field (if the session was started with -a <agent>)
275+ if let Some ( ref agent_name) = export. session . agent {
276+ if !local_agents. contains ( agent_name) {
277+ missing_agents. push ( agent_name. clone ( ) ) ;
278+ }
279+ }
280+
281+ // Check agent_refs from the export metadata (pre-extracted @mentions)
282+ if let Some ( ref agent_refs) = export. session . agent_refs {
283+ for agent_ref in agent_refs {
284+ if !local_agents. contains ( agent_ref) && !missing_agents. contains ( agent_ref) {
285+ missing_agents. push ( agent_ref. clone ( ) ) ;
286+ }
287+ }
288+ }
289+
290+ // Also scan messages for @agent mentions (in case they weren't pre-extracted)
291+ let re = regex:: Regex :: new ( r"@([a-zA-Z][a-zA-Z0-9_-]*)" ) . unwrap ( ) ;
292+ for message in & export. messages {
293+ for cap in re. captures_iter ( & message. content ) {
294+ if let Some ( agent_name) = cap. get ( 1 ) {
295+ let name = agent_name. as_str ( ) . to_string ( ) ;
296+ if !local_agents. contains ( & name) && !missing_agents. contains ( & name) {
297+ missing_agents. push ( name) ;
298+ }
299+ }
300+ }
301+ }
302+
303+ missing_agents. sort ( ) ;
304+ Ok ( missing_agents)
305+ }
306+
238307/// Convert an export message to a protocol event.
239308fn message_to_event ( message : & ExportMessage , turn_id : & mut u64 , cwd : & PathBuf ) -> Result < Event > {
240309 let event_msg = match message. role . as_str ( ) {
@@ -422,4 +491,79 @@ mod tests {
422491 assert_eq ! ( preview_len, 200 ) ;
423492 assert_eq ! ( & long_content[ ..preview_len] . len( ) , & 200 ) ;
424493 }
494+
495+ #[ test]
496+ fn test_validate_agent_references_with_missing_agents ( ) {
497+ use crate :: export_cmd:: SessionMetadata ;
498+
499+ // Create an export with agent references that don't exist locally
500+ let export = SessionExport {
501+ version : 1 ,
502+ session : SessionMetadata {
503+ id : "test-123" . to_string ( ) ,
504+ title : None ,
505+ created_at : "2024-01-01T00:00:00Z" . to_string ( ) ,
506+ cwd : None ,
507+ model : None ,
508+ agent : Some ( "custom-nonexistent-agent" . to_string ( ) ) ,
509+ agent_refs : Some ( vec ! [ "another-nonexistent" . to_string( ) ] ) ,
510+ } ,
511+ messages : vec ! [ ExportMessage {
512+ role: "user" . to_string( ) ,
513+ content: "@yet-another-missing help me" . to_string( ) ,
514+ tool_calls: None ,
515+ tool_call_id: None ,
516+ timestamp: None ,
517+ } ] ,
518+ } ;
519+
520+ let missing = validate_agent_references ( & export) . unwrap ( ) ;
521+
522+ // Should find at least the explicitly nonexistent ones
523+ // (built-in agents like 'explore', 'research' will exist)
524+ assert ! ( missing. contains( & "custom-nonexistent-agent" . to_string( ) ) ) ;
525+ assert ! ( missing. contains( & "another-nonexistent" . to_string( ) ) ) ;
526+ assert ! ( missing. contains( & "yet-another-missing" . to_string( ) ) ) ;
527+ }
528+
529+ #[ test]
530+ fn test_validate_agent_references_with_builtin_agents ( ) {
531+ use crate :: export_cmd:: SessionMetadata ;
532+
533+ // Create an export referencing only built-in agents
534+ let export = SessionExport {
535+ version : 1 ,
536+ session : SessionMetadata {
537+ id : "test-123" . to_string ( ) ,
538+ title : None ,
539+ created_at : "2024-01-01T00:00:00Z" . to_string ( ) ,
540+ cwd : None ,
541+ model : None ,
542+ agent : None ,
543+ agent_refs : None ,
544+ } ,
545+ messages : vec ! [
546+ ExportMessage {
547+ role: "user" . to_string( ) ,
548+ content: "@build help me compile" . to_string( ) ,
549+ tool_calls: None ,
550+ tool_call_id: None ,
551+ timestamp: None ,
552+ } ,
553+ ExportMessage {
554+ role: "user" . to_string( ) ,
555+ content: "@plan create a plan" . to_string( ) ,
556+ tool_calls: None ,
557+ tool_call_id: None ,
558+ timestamp: None ,
559+ } ,
560+ ] ,
561+ } ;
562+
563+ let missing = validate_agent_references ( & export) . unwrap ( ) ;
564+
565+ // Built-in agents should not be reported as missing
566+ assert ! ( !missing. contains( & "build" . to_string( ) ) ) ;
567+ assert ! ( !missing. contains( & "plan" . to_string( ) ) ) ;
568+ }
425569}
0 commit comments