Skip to content

Commit c6622c5

Browse files
committed
merge main & fix ci
2 parents c6cca7f + 46ddc72 commit c6622c5

File tree

6 files changed

+177
-13
lines changed

6 files changed

+177
-13
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ pub struct Agent {
161161
/// you configure in the mcpServers field in this config
162162
#[serde(default)]
163163
pub use_legacy_mcp_json: bool,
164+
/// The model ID to use for this agent. If not specified, uses the default model.
165+
#[serde(default)]
166+
pub model: Option<String>,
164167
#[serde(skip)]
165168
pub path: Option<PathBuf>,
166169
}
@@ -181,13 +184,19 @@ impl Default for Agent {
181184
set.extend(default_approve);
182185
set
183186
},
184-
resources: vec!["file://AmazonQ.md", "file://AGENTS.md", "file://README.md", "file://.amazonq/rules/**/*.md"]
185-
.into_iter()
186-
.map(Into::into)
187-
.collect::<Vec<_>>(),
187+
resources: vec![
188+
"file://AmazonQ.md",
189+
"file://AGENTS.md",
190+
"file://README.md",
191+
"file://.amazonq/rules/**/*.md",
192+
]
193+
.into_iter()
194+
.map(Into::into)
195+
.collect::<Vec<_>>(),
188196
hooks: Default::default(),
189197
tools_settings: Default::default(),
190198
use_legacy_mcp_json: true,
199+
model: None,
191200
path: None,
192201
}
193202
}
@@ -1215,6 +1224,7 @@ mod tests {
12151224
resources: Vec::new(),
12161225
hooks: Default::default(),
12171226
use_legacy_mcp_json: false,
1227+
model: None,
12181228
path: None,
12191229
};
12201230

@@ -1285,4 +1295,62 @@ mod tests {
12851295
label
12861296
);
12871297
}
1298+
1299+
#[test]
1300+
fn test_agent_model_field() {
1301+
// Test deserialization with model field
1302+
let agent_json = r#"{
1303+
"name": "test-agent",
1304+
"model": "claude-sonnet-4"
1305+
}"#;
1306+
1307+
let agent: Agent = serde_json::from_str(agent_json).expect("Failed to deserialize agent with model");
1308+
assert_eq!(agent.model, Some("claude-sonnet-4".to_string()));
1309+
1310+
// Test default agent has no model
1311+
let default_agent = Agent::default();
1312+
assert_eq!(default_agent.model, None);
1313+
1314+
// Test serialization includes model field
1315+
let agent_with_model = Agent {
1316+
model: Some("test-model".to_string()),
1317+
..Default::default()
1318+
};
1319+
let serialized = serde_json::to_string(&agent_with_model).expect("Failed to serialize");
1320+
assert!(serialized.contains("\"model\":\"test-model\""));
1321+
}
1322+
1323+
#[test]
1324+
fn test_agent_model_fallback_priority() {
1325+
// Test that agent model is checked and falls back correctly
1326+
let mut agents = Agents::default();
1327+
1328+
// Create agent with unavailable model
1329+
let agent_with_invalid_model = Agent {
1330+
name: "test-agent".to_string(),
1331+
model: Some("unavailable-model".to_string()),
1332+
..Default::default()
1333+
};
1334+
1335+
agents.agents.insert("test-agent".to_string(), agent_with_invalid_model);
1336+
agents.active_idx = "test-agent".to_string();
1337+
1338+
// Verify the agent has the model set
1339+
assert_eq!(
1340+
agents.get_active().and_then(|a| a.model.as_ref()),
1341+
Some(&"unavailable-model".to_string())
1342+
);
1343+
1344+
// Test agent without model
1345+
let agent_without_model = Agent {
1346+
name: "no-model-agent".to_string(),
1347+
model: None,
1348+
..Default::default()
1349+
};
1350+
1351+
agents.agents.insert("no-model-agent".to_string(), agent_without_model);
1352+
agents.active_idx = "no-model-agent".to_string();
1353+
1354+
assert_eq!(agents.get_active().and_then(|a| a.model.as_ref()), None);
1355+
}
12881356
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use clap::{
4343
};
4444
use cli::compact::CompactStrategy;
4545
use cli::model::{
46+
find_model,
4647
get_available_models,
4748
select_model,
4849
};
@@ -142,7 +143,6 @@ use crate::cli::TodoListState;
142143
use crate::cli::agent::Agents;
143144
use crate::cli::chat::cli::SlashCommand;
144145
use crate::cli::chat::cli::editor::open_editor;
145-
use crate::cli::chat::cli::model::find_model;
146146
use crate::cli::chat::cli::prompts::{
147147
GetPromptError,
148148
PromptsSubcommand,
@@ -354,7 +354,19 @@ impl ChatArgs {
354354
// If modelId is specified, verify it exists before starting the chat
355355
// Otherwise, CLI will use a default model when starting chat
356356
let (models, default_model_opt) = get_available_models(os).await?;
357+
// Fallback logic: try user's saved default, then system default
358+
let fallback_model_id = || {
359+
if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
360+
find_model(&models, &saved)
361+
.map(|m| m.model_id.clone())
362+
.or(Some(default_model_opt.model_id.clone()))
363+
} else {
364+
Some(default_model_opt.model_id.clone())
365+
}
366+
};
367+
357368
let model_id: Option<String> = if let Some(requested) = self.model.as_ref() {
369+
// CLI argument takes highest priority
358370
if let Some(m) = find_model(&models, requested) {
359371
Some(m.model_id.clone())
360372
} else {
@@ -365,12 +377,26 @@ impl ChatArgs {
365377
.join(", ");
366378
bail!("Model '{}' does not exist. Available models: {}", requested, available);
367379
}
368-
} else if let Some(saved) = os.database.settings.get_string(Setting::ChatDefaultModel) {
369-
find_model(&models, &saved)
370-
.map(|m| m.model_id.clone())
371-
.or(Some(default_model_opt.model_id.clone()))
380+
} else if let Some(agent_model) = agents.get_active().and_then(|a| a.model.as_ref()) {
381+
// Agent model takes second priority
382+
if let Some(m) = find_model(&models, agent_model) {
383+
Some(m.model_id.clone())
384+
} else {
385+
let _ = execute!(
386+
stderr,
387+
style::SetForegroundColor(Color::Yellow),
388+
style::Print("WARNING: "),
389+
style::SetForegroundColor(Color::Reset),
390+
style::Print("Agent specifies model '"),
391+
style::SetForegroundColor(Color::Cyan),
392+
style::Print(agent_model),
393+
style::SetForegroundColor(Color::Reset),
394+
style::Print("' which is not available. Falling back to configured defaults.\n"),
395+
);
396+
fallback_model_id()
397+
}
372398
} else {
373-
Some(default_model_opt.model_id.clone())
399+
fallback_model_id()
374400
};
375401

376402
let (prompt_request_sender, prompt_request_receiver) = tokio::sync::broadcast::channel::<PromptQuery>(5);
@@ -3346,6 +3372,7 @@ mod tests {
33463372
tool_config,
33473373
true,
33483374
false,
3375+
None,
33493376
)
33503377
.await
33513378
.unwrap()
@@ -3586,6 +3613,7 @@ mod tests {
35863613
tool_config,
35873614
true,
35883615
false,
3616+
None,
35893617
)
35903618
.await
35913619
.unwrap()
@@ -3661,6 +3689,7 @@ mod tests {
36613689
tool_config,
36623690
true,
36633691
false,
3692+
None,
36643693
)
36653694
.await
36663695
.unwrap()
@@ -3712,6 +3741,7 @@ mod tests {
37123741
tool_config,
37133742
true,
37143743
false,
3744+
None,
37153745
)
37163746
.await
37173747
.unwrap()

crates/chat-cli/src/util/directories.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ pub fn canonicalizes_path(os: &Os, path_as_str: &str) -> Result<String> {
193193
/// patterns to exist in a globset.
194194
pub fn add_gitignore_globs(builder: &mut GlobSetBuilder, path: &str) -> Result<()> {
195195
let glob_for_file = Glob::new(path)?;
196-
let glob_for_dir = Glob::new(&format!("{path}/**"))?;
196+
197+
// remove existing slash in path so we don't end up with double slash
198+
// Glob doesn't normalize the path so it doesn't work with double slash
199+
let dir_pattern: String = format!("{}/**", path.trim_end_matches('/'));
200+
let glob_for_dir = Glob::new(&dir_pattern)?;
197201

198202
builder.add(glob_for_file);
199203
builder.add(glob_for_dir);
@@ -278,6 +282,40 @@ mod linux_tests {
278282
assert!(logs_dir().is_ok());
279283
assert!(settings_path().is_ok());
280284
}
285+
286+
#[test]
287+
fn test_add_gitignore_globs() {
288+
let direct_file = "/home/user/a.txt";
289+
let nested_file = "/home/user/folder/a.txt";
290+
let other_file = "/home/admin/a.txt";
291+
292+
// Case 1: Path with trailing slash
293+
let mut builder1 = GlobSetBuilder::new();
294+
add_gitignore_globs(&mut builder1, "/home/user/").unwrap();
295+
let globset1 = builder1.build().unwrap();
296+
297+
assert!(globset1.is_match(direct_file));
298+
assert!(globset1.is_match(nested_file));
299+
assert!(!globset1.is_match(other_file));
300+
301+
// Case 2: Path without trailing slash - should behave same as case 1
302+
let mut builder2 = GlobSetBuilder::new();
303+
add_gitignore_globs(&mut builder2, "/home/user").unwrap();
304+
let globset2 = builder2.build().unwrap();
305+
306+
assert!(globset2.is_match(direct_file));
307+
assert!(globset2.is_match(nested_file));
308+
assert!(!globset1.is_match(other_file));
309+
310+
// Case 3: File path - should only match exact file
311+
let mut builder3 = GlobSetBuilder::new();
312+
add_gitignore_globs(&mut builder3, "/home/user/a.txt").unwrap();
313+
let globset3 = builder3.build().unwrap();
314+
315+
assert!(globset3.is_match(direct_file));
316+
assert!(!globset3.is_match(nested_file));
317+
assert!(!globset1.is_match(other_file));
318+
}
281319
}
282320

283321
// TODO(grant): Add back path tests on linux

docs/agent-format.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
The agent configuration file for each agent is a JSON file. The filename (without the `.json` extension) becomes the agent's name. It contains configuration needed to instantiate and run the agent.
44

5+
> [!TIP]
6+
> We recommend using the `/agent generate` slash command within your active Q session to intelligently generate your agent configuration with the help of Q.
7+
58
Every agent configuration file can include the following sections:
69

710
- [`name`](#name-field) — The name of the agent (optional, derived from filename if not specified).
@@ -15,6 +18,7 @@ Every agent configuration file can include the following sections:
1518
- [`resources`](#resources-field) — Resources available to the agent.
1619
- [`hooks`](#hooks-field) — Commands run at specific trigger points.
1720
- [`useLegacyMcpJson`](#uselegacymcpjson-field) — Whether to include legacy MCP configuration.
21+
- [`model`](#model-field) — The model ID to use for this agent.
1822

1923
## Name Field
2024

@@ -290,6 +294,20 @@ The `useLegacyMcpJson` field determines whether to include MCP servers defined i
290294

291295
When set to `true`, the agent will have access to all MCP servers defined in the global and local configurations in addition to those defined in the agent's `mcpServers` field.
292296

297+
## Model Field
298+
299+
The `model` field specifies the model ID to use for this agent. If not specified, the agent will use the default model.
300+
301+
```json
302+
{
303+
"model": "claude-sonnet-4"
304+
}
305+
```
306+
307+
The model ID must match one of the available models returned by the Q CLI's model service. You can see available models by using the `/model` command in an active chat session.
308+
309+
If the specified model is not available, the agent will fall back to the default model and display a warning.
310+
293311
## Complete Example
294312

295313
Here's a complete example of an agent configuration file:
@@ -348,6 +366,7 @@ Here's a complete example of an agent configuration file:
348366
}
349367
]
350368
},
351-
"useLegacyMcpJson": true
369+
"useLegacyMcpJson": true,
370+
"model": "claude-sonnet-4"
352371
}
353372
```

docs/knowledge-management.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
The /knowledge command provides persistent knowledge base functionality for Amazon Q CLI, allowing you to store, search, and manage contextual information that persists across chat sessions.
44

5-
> Note: This is a beta feature that must be enabled before use.
5+
> [!NOTE]
6+
> This is a beta feature that must be enabled before use.
67
78
## Getting Started
89

schemas/agent-v1.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@
159159
"description": "Whether or not to include the legacy ~/.aws/amazonq/mcp.json in the agent\nYou can reference tools brought in by these servers as just as you would with the servers\nyou configure in the mcpServers field in this config",
160160
"type": "boolean",
161161
"default": false
162+
},
163+
"model": {
164+
"description": "The model ID to use for this agent. If not specified, uses the default model.",
165+
"type": [
166+
"string",
167+
"null"
168+
],
169+
"default": null
162170
}
163171
},
164172
"additionalProperties": false,

0 commit comments

Comments
 (0)