@@ -66,8 +66,30 @@ use crate::util::{
66
66
/// An [Agent] is a declarative way of configuring a given instance of q chat. Currently, it is
67
67
/// impacting q chat in via influenicng [ContextManager] and [ToolManager].
68
68
/// Changes made to [ContextManager] and [ToolManager] do not persist across sessions.
69
+ ///
70
+ /// To increase the usability of the agent config, (both from the perspective of CLI and the users
71
+ /// who would need to write these config), the agent config has two states of existence: "cold" and
72
+ /// "warm".
73
+ ///
74
+ /// A "cold" state describes the config as it is written. And a "warm" state is an alternate form
75
+ /// of the same config, modified for the convenience of the business logic that relies on it in the
76
+ /// application.
77
+ ///
78
+ /// For example, the "cold" state does not require the field of "path" to be populated. This is
79
+ /// because it would be redundant and tedious for user to have to write the path of the file they
80
+ /// had created in said file. This field is thus populated during its parsing.
81
+ ///
82
+ /// Another example is the mcp config. To support backwards compatibility of users existing global
83
+ /// mcp.json, we allow users to supply a flag to denote whether they would want to include servers
84
+ /// from the legacy global mcp.json. If this flag exists, we would need to read the legacy mcp
85
+ /// config and merge it with what is in the agent mcp servers field. Conversely, when we write this
86
+ /// config to file, we would want to filter out the servers that belong only in the mcp.json.
87
+ ///
88
+ /// Where agents are instantiated from their config, we would need to convert them from "cold" to
89
+ /// "warm".
69
90
#[ derive( Debug , Clone , Serialize , Deserialize , Eq , PartialEq , JsonSchema ) ]
70
91
#[ serde( rename_all = "camelCase" ) ]
92
+ #[ schemars( description = "An Agent is a declarative way of configuring a given instance of q chat." ) ]
71
93
pub struct Agent {
72
94
/// Agent names are derived from the file name. Thus they are skipped for
73
95
/// serializing
@@ -105,6 +127,11 @@ pub struct Agent {
105
127
#[ serde( default ) ]
106
128
#[ schemars( schema_with = "tool_settings_schema" ) ]
107
129
pub tools_settings : HashMap < ToolSettingTarget , serde_json:: Value > ,
130
+ /// Whether or not to include the legacy ~/.aws/amazonq/mcp.json in the agent
131
+ /// You can reference tools brought in by these servers as just as you would with the servers
132
+ /// you configure in the mcpServers field in this config
133
+ #[ serde( default ) ]
134
+ pub use_legacy_mcp_json : bool ,
108
135
#[ serde( skip) ]
109
136
pub path : Option < PathBuf > ,
110
137
}
@@ -116,7 +143,7 @@ impl Default for Agent {
116
143
description : Some ( "Default agent" . to_string ( ) ) ,
117
144
prompt : Default :: default ( ) ,
118
145
mcp_servers : Default :: default ( ) ,
119
- tools : NATIVE_TOOLS . iter ( ) . copied ( ) . map ( str :: to_string) . collect :: < Vec < _ > > ( ) ,
146
+ tools : vec ! [ "*" . to_string( ) ] ,
120
147
tool_aliases : Default :: default ( ) ,
121
148
allowed_tools : {
122
149
let mut set = HashSet :: < String > :: new ( ) ;
@@ -130,20 +157,83 @@ impl Default for Agent {
130
157
. collect :: < Vec < _ > > ( ) ,
131
158
hooks : Default :: default ( ) ,
132
159
tools_settings : Default :: default ( ) ,
160
+ use_legacy_mcp_json : true ,
133
161
path : None ,
134
162
}
135
163
}
136
164
}
137
165
138
166
impl Agent {
167
+ /// This function mutates the agent to a state that is writable.
168
+ /// Practically this means reverting some fields back to their original values as they were
169
+ /// written in the config.
170
+ fn freeze ( & mut self ) -> eyre:: Result < ( ) > {
171
+ let Self { mcp_servers, .. } = self ;
172
+
173
+ mcp_servers
174
+ . mcp_servers
175
+ . retain ( |_name, config| !config. is_from_legacy_mcp_json ) ;
176
+
177
+ Ok ( ( ) )
178
+ }
179
+
180
+ /// This function mutates the agent to a state that is usable for runtime.
181
+ /// Practically this means to convert some of the fields value to their usable counterpart.
182
+ /// For example, we populate the agent with its file name, convert the mcp array to actual
183
+ /// mcp config and populate the agent file path.
184
+ fn thaw ( & mut self , path : & Path , global_mcp_config : Option < & McpServerConfig > ) -> eyre:: Result < ( ) > {
185
+ let Self { mcp_servers, .. } = self ;
186
+
187
+ let name = path
188
+ . file_stem ( )
189
+ . ok_or ( eyre:: eyre!( "Missing valid file name" ) ) ?
190
+ . to_string_lossy ( )
191
+ . to_string ( ) ;
192
+
193
+ self . name = name. clone ( ) ;
194
+
195
+ if let ( true , Some ( global_mcp_config) ) = ( self . use_legacy_mcp_json , global_mcp_config) {
196
+ let mut stderr = std:: io:: stderr ( ) ;
197
+ for ( name, legacy_server) in & global_mcp_config. mcp_servers {
198
+ if mcp_servers. mcp_servers . contains_key ( name) {
199
+ let _ = queue ! (
200
+ stderr,
201
+ style:: SetForegroundColor ( Color :: Yellow ) ,
202
+ style:: Print ( "WARNING: " ) ,
203
+ style:: ResetColor ,
204
+ style:: Print ( "MCP server '" ) ,
205
+ style:: SetForegroundColor ( Color :: Green ) ,
206
+ style:: Print ( name) ,
207
+ style:: ResetColor ,
208
+ style:: Print (
209
+ "' is already configured in agent config. Skipping duplicate from legacy mcp.json.\n "
210
+ )
211
+ ) ;
212
+ continue ;
213
+ }
214
+ let mut server_clone = legacy_server. clone ( ) ;
215
+ server_clone. is_from_legacy_mcp_json = true ;
216
+ mcp_servers. mcp_servers . insert ( name. clone ( ) , server_clone) ;
217
+ }
218
+ }
219
+
220
+ Ok ( ( ) )
221
+ }
222
+
223
+ pub fn to_str_pretty ( & self ) -> eyre:: Result < String > {
224
+ let mut agent_clone = self . clone ( ) ;
225
+ agent_clone. freeze ( ) ?;
226
+ Ok ( serde_json:: to_string_pretty ( & agent_clone) ?)
227
+ }
228
+
139
229
/// Retrieves an agent by name. It does so via first seeking the given agent under local dir,
140
230
/// and falling back to global dir if it does not exist in local.
141
231
pub async fn get_agent_by_name ( os : & Os , agent_name : & str ) -> eyre:: Result < ( Agent , PathBuf ) > {
142
232
let config_path: Result < PathBuf , PathBuf > = ' config: {
143
233
// local first, and then fall back to looking at global
144
- let local_config_dir = directories:: chat_local_agent_dir ( ) ?. join ( agent_name) ;
234
+ let local_config_dir = directories:: chat_local_agent_dir ( ) ?. join ( format ! ( "{ agent_name}.json" ) ) ;
145
235
if os. fs . exists ( & local_config_dir) {
146
- break ' config Ok :: < PathBuf , PathBuf > ( local_config_dir) ;
236
+ break ' config Ok ( local_config_dir) ;
147
237
}
148
238
149
239
let global_config_dir = directories:: chat_global_agent_path ( os) ?. join ( format ! ( "{agent_name}.json" ) ) ;
@@ -157,23 +247,18 @@ impl Agent {
157
247
match config_path {
158
248
Ok ( config_path) => {
159
249
let content = os. fs . read ( & config_path) . await ?;
160
- Ok ( ( serde_json:: from_slice :: < Agent > ( & content) ?, config_path) )
161
- } ,
162
- Err ( global_config_dir) if agent_name == "default" => {
163
- os. fs
164
- . create_dir_all (
165
- global_config_dir
166
- . parent ( )
167
- . ok_or ( eyre:: eyre!( "Failed to retrieve global agent config parent path" ) ) ?,
168
- )
169
- . await ?;
170
- os. fs . create_new ( & global_config_dir) . await ?;
171
-
172
- let default_agent = Agent :: default ( ) ;
173
- let content = serde_json:: to_string_pretty ( & default_agent) ?;
174
- os. fs . write ( & global_config_dir, content. as_bytes ( ) ) . await ?;
175
-
176
- Ok ( ( default_agent, global_config_dir) )
250
+ let mut agent = serde_json:: from_slice :: < Agent > ( & content) ?;
251
+
252
+ let global_mcp_path = directories:: chat_legacy_mcp_config ( os) ?;
253
+ let global_mcp_config = match McpServerConfig :: load_from_file ( os, global_mcp_path) . await {
254
+ Ok ( config) => Some ( config) ,
255
+ Err ( e) => {
256
+ tracing:: error!( "Error loading global mcp json path: {e}." ) ;
257
+ None
258
+ } ,
259
+ } ;
260
+ agent. thaw ( & config_path, global_mcp_config. as_ref ( ) ) ?;
261
+ Ok ( ( agent, config_path) )
177
262
} ,
178
263
_ => bail ! ( "Agent {agent_name} does not exist" ) ,
179
264
}
@@ -252,7 +337,8 @@ impl Agents {
252
337
path : Some ( agent_path. clone ( ) ) ,
253
338
..Default :: default ( )
254
339
} ;
255
- let contents = serde_json:: to_string_pretty ( & agent)
340
+ let contents = agent
341
+ . to_str_pretty ( )
256
342
. map_err ( |e| eyre:: eyre!( "Failed to serialize profile configuration: {}" , e) ) ?;
257
343
258
344
if let Some ( parent) = agent_path. parent ( ) {
@@ -307,6 +393,8 @@ impl Agents {
307
393
vec ! [ ]
308
394
} ;
309
395
396
+ let mut global_mcp_config = None :: < McpServerConfig > ;
397
+
310
398
let mut local_agents = ' local: {
311
399
// We could be launching from the home dir, in which case the global and local agents
312
400
// are the same set of agents. If that is the case, we simply skip this.
@@ -324,7 +412,7 @@ impl Agents {
324
412
let Ok ( files) = os. fs . read_dir ( path) . await else {
325
413
break ' local Vec :: < Agent > :: new ( ) ;
326
414
} ;
327
- load_agents_from_entries ( files) . await
415
+ load_agents_from_entries ( files, os , & mut global_mcp_config ) . await
328
416
} ;
329
417
330
418
let mut global_agents = ' global: {
@@ -342,7 +430,7 @@ impl Agents {
342
430
break ' global Vec :: < Agent > :: new ( ) ;
343
431
} ,
344
432
} ;
345
- load_agents_from_entries ( files) . await
433
+ load_agents_from_entries ( files, os , & mut global_mcp_config ) . await
346
434
}
347
435
. into_iter ( )
348
436
. chain ( new_agents)
@@ -385,7 +473,7 @@ impl Agents {
385
473
} ,
386
474
..Default :: default ( )
387
475
} ;
388
- let Ok ( content) = serde_json :: to_string_pretty ( & example_agent) else {
476
+ let Ok ( content) = example_agent. to_str_pretty ( ) else {
389
477
error ! ( "Error serializing example agent config" ) ;
390
478
break ' example_config;
391
479
} ;
@@ -522,8 +610,13 @@ impl Agents {
522
610
}
523
611
}
524
612
525
- async fn load_agents_from_entries ( mut files : ReadDir ) -> Vec < Agent > {
613
+ async fn load_agents_from_entries (
614
+ mut files : ReadDir ,
615
+ os : & Os ,
616
+ global_mcp_config : & mut Option < McpServerConfig > ,
617
+ ) -> Vec < Agent > {
526
618
let mut res = Vec :: < Agent > :: new ( ) ;
619
+
527
620
while let Ok ( Some ( file) ) = files. next_entry ( ) . await {
528
621
let file_path = & file. path ( ) ;
529
622
if file_path
@@ -539,6 +632,7 @@ async fn load_agents_from_entries(mut files: ReadDir) -> Vec<Agent> {
539
632
continue ;
540
633
} ,
541
634
} ;
635
+
542
636
let mut agent = match serde_json:: from_slice :: < Agent > ( & content) {
543
637
Ok ( mut agent) => {
544
638
agent. path = Some ( file_path. clone ( ) ) ;
@@ -550,13 +644,34 @@ async fn load_agents_from_entries(mut files: ReadDir) -> Vec<Agent> {
550
644
continue ;
551
645
} ,
552
646
} ;
553
- if let Some ( name) = Path :: new ( & file. file_name ( ) ) . file_stem ( ) {
554
- agent. name = name. to_string_lossy ( ) . to_string ( ) ;
555
- res. push ( agent) ;
556
- } else {
557
- let file_path = file_path. to_string_lossy ( ) ;
558
- tracing:: error!( "Unable to determine agent name from config file at {file_path}, skipping" ) ;
647
+
648
+ // The agent config could have use_legacy_mcp_json set to true but not have a valid
649
+ // global mcp.json. We would still need to carry on loading the config.
650
+ ' load_legacy_mcp_json: {
651
+ if agent. use_legacy_mcp_json && global_mcp_config. is_none ( ) {
652
+ let Ok ( global_mcp_path) = directories:: chat_legacy_mcp_config ( os) else {
653
+ tracing:: error!( "Error obtaining legacy mcp json path. Skipping" ) ;
654
+ break ' load_legacy_mcp_json;
655
+ } ;
656
+ let legacy_mcp_config = match McpServerConfig :: load_from_file ( os, global_mcp_path) . await {
657
+ Ok ( config) => config,
658
+ Err ( e) => {
659
+ tracing:: error!( "Error loading global mcp json path: {e}. Skipping" ) ;
660
+ break ' load_legacy_mcp_json;
661
+ } ,
662
+ } ;
663
+ global_mcp_config. replace ( legacy_mcp_config) ;
664
+ }
559
665
}
666
+
667
+ if let Err ( e) = agent. thaw ( file_path, global_mcp_config. as_ref ( ) ) {
668
+ tracing:: error!(
669
+ "Error transforming agent at {} to usable state: {e}. Skipping" ,
670
+ file_path. display( )
671
+ ) ;
672
+ } ;
673
+
674
+ res. push ( agent) ;
560
675
}
561
676
}
562
677
res
@@ -806,11 +921,4 @@ mod tests {
806
921
assert ! ( validate_agent_name( "invalid!" ) . is_err( ) ) ;
807
922
assert ! ( validate_agent_name( "invalid space" ) . is_err( ) ) ;
808
923
}
809
-
810
- #[ test]
811
- fn test_schema_gen ( ) {
812
- use schemars:: schema_for;
813
- let schema = schema_for ! ( Agent ) ;
814
- println ! ( "Schema for agent: {}" , serde_json:: to_string_pretty( & schema) . unwrap( ) ) ;
815
- }
816
924
}
0 commit comments