Skip to content

Commit dc0974f

Browse files
authored
feat(agent): adds root and slash command to set user defined default (#2420)
1 parent 96c62f0 commit dc0974f

File tree

4 files changed

+106
-27
lines changed

4 files changed

+106
-27
lines changed

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

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ use crate::util::{
7171
directories,
7272
};
7373

74+
const DEFAULT_AGENT_NAME: &str = "q_cli_default";
75+
7476
#[derive(Debug, Error)]
7577
pub enum AgentConfigError {
7678
#[error("Json supplied at {} is invalid: {}", path.display(), error)]
@@ -86,8 +88,6 @@ pub enum AgentConfigError {
8688
Directories(#[from] util::directories::DirectoryError),
8789
#[error("Encountered io error: {0}")]
8890
Io(#[from] std::io::Error),
89-
#[error("Agent path missing file name")]
90-
MissingFilename,
9191
#[error("Failed to parse legacy mcp config: {0}")]
9292
BadLegacyMcpConfig(#[from] eyre::Report),
9393
}
@@ -120,13 +120,14 @@ pub enum AgentConfigError {
120120
#[serde(rename_all = "camelCase", deny_unknown_fields)]
121121
#[schemars(description = "An Agent is a declarative way of configuring a given instance of q chat.")]
122122
pub struct Agent {
123-
/// Agent names are optional. If they are not provided they are derived from the file name
124-
#[serde(default)]
123+
/// Name of the agent
125124
pub name: String,
125+
/// Version of the agent config
126+
pub version: String,
126127
/// This field is not model facing and is mostly here for users to discern between agents
127128
#[serde(default)]
128129
pub description: Option<String>,
129-
/// (NOT YET IMPLEMENTED) The intention for this field is to provide high level context to the
130+
/// The intention for this field is to provide high level context to the
130131
/// agent. This should be seen as the same category of context as a system prompt.
131132
#[serde(default)]
132133
pub prompt: Option<String>,
@@ -167,7 +168,8 @@ pub struct Agent {
167168
impl Default for Agent {
168169
fn default() -> Self {
169170
Self {
170-
name: "default".to_string(),
171+
name: DEFAULT_AGENT_NAME.to_string(),
172+
version: "0.1.0".to_string(),
171173
description: Some("Default agent".to_string()),
172174
prompt: Default::default(),
173175
mcp_servers: Default::default(),
@@ -205,21 +207,10 @@ impl Agent {
205207

206208
/// This function mutates the agent to a state that is usable for runtime.
207209
/// Practically this means to convert some of the fields value to their usable counterpart.
208-
/// For example, we populate the agent with its file name, convert the mcp array to actual
209-
/// mcp config and populate the agent file path.
210+
/// For example, converting the mcp array to actual mcp config and populate the agent file path.
210211
fn thaw(&mut self, path: &Path, global_mcp_config: Option<&McpServerConfig>) -> Result<(), AgentConfigError> {
211212
let Self { mcp_servers, .. } = self;
212213

213-
if self.name.is_empty() {
214-
let name = path
215-
.file_stem()
216-
.ok_or(AgentConfigError::MissingFilename)?
217-
.to_string_lossy()
218-
.to_string();
219-
220-
self.name = name;
221-
}
222-
223214
self.path = Some(path.to_path_buf());
224215

225216
if let (true, Some(global_mcp_config)) = (self.use_legacy_mcp_json, global_mcp_config) {
@@ -643,7 +634,7 @@ impl Agents {
643634
agent
644635
});
645636

646-
"default".to_string()
637+
DEFAULT_AGENT_NAME.to_string()
647638
};
648639

649640
let _ = output.flush();
@@ -802,6 +793,8 @@ mod tests {
802793

803794
const INPUT: &str = r#"
804795
{
796+
"name": "some_agent",
797+
"version": "0.1.0",
805798
"description": "My developer agent is used for small development tasks like solving open issues.",
806799
"prompt": "You are a principal developer who uses multiple agents to accomplish difficult engineering tasks",
807800
"mcpServers": {
@@ -843,11 +836,12 @@ mod tests {
843836
assert!(collection.get_active().is_none());
844837

845838
let agent = Agent::default();
846-
collection.agents.insert("default".to_string(), agent);
847-
collection.active_idx = "default".to_string();
839+
let agent_name = agent.name.clone();
840+
collection.agents.insert(agent_name.clone(), agent);
841+
collection.active_idx = agent_name.clone();
848842

849843
assert!(collection.get_active().is_some());
850-
assert_eq!(collection.get_active().unwrap().name, "default");
844+
assert_eq!(collection.get_active().unwrap().name, agent_name);
851845
}
852846

853847
#[test]

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ pub enum AgentSubcommands {
7070
#[arg(long)]
7171
force: bool,
7272
},
73+
/// Define a default agent to use when q chat launches
74+
SetDefault {
75+
#[arg(long, short)]
76+
name: String,
77+
},
7378
}
7479

7580
#[derive(Debug, Clone, PartialEq, Eq, Default, Args)]
@@ -262,6 +267,35 @@ impl AgentArgs {
262267
},
263268
}
264269
},
270+
Some(AgentSubcommands::SetDefault { name }) => {
271+
let mut agents = Agents::load(os, None, true, &mut stderr).await.0;
272+
match agents.switch(&name) {
273+
Ok(agent) => {
274+
os.database
275+
.settings
276+
.set(Setting::ChatDefaultAgent, agent.name.clone())
277+
.await?;
278+
279+
let _ = queue!(
280+
stderr,
281+
style::SetForegroundColor(Color::Green),
282+
style::Print("✓ Default agent set to '"),
283+
style::Print(&agent.name),
284+
style::Print("'. This will take effect the next time q chat is launched.\n"),
285+
style::ResetColor,
286+
);
287+
},
288+
Err(e) => {
289+
let _ = queue!(
290+
stderr,
291+
style::SetForegroundColor(Color::Red),
292+
style::Print("Error: "),
293+
style::ResetColor,
294+
style::Print(format!("Failed to set default agent: {e}\n")),
295+
);
296+
},
297+
}
298+
},
265299
}
266300

267301
Ok(ExitCode::SUCCESS)
@@ -301,11 +335,17 @@ pub async fn create_agent(
301335
}
302336

303337
let prepopulated_content = if let Some(from) = from {
304-
let agent_to_copy = agents.switch(from.as_str())?;
305-
agent_to_copy.to_str_pretty()?
338+
let mut agent_to_copy = agents.switch(from.as_str())?.clone();
339+
agent_to_copy.name = name.clone();
340+
agent_to_copy
306341
} else {
307-
Default::default()
308-
};
342+
Agent {
343+
name: name.clone(),
344+
description: Some(Default::default()),
345+
..Default::default()
346+
}
347+
}
348+
.to_str_pretty()?;
309349
let path_with_file_name = path.join(format!("{name}.json"));
310350

311351
if !path.exists() {

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::cli::chat::{
3333
ChatSession,
3434
ChatState,
3535
};
36+
use crate::database::settings::Setting;
3637
use crate::os::Os;
3738
use crate::util::directories::chat_global_agent_path;
3839

@@ -82,6 +83,11 @@ pub enum AgentSubcommand {
8283
},
8384
/// Show agent config schema
8485
Schema,
86+
/// Define a default agent to use when q chat launches
87+
SetDefault {
88+
#[arg(long, short)]
89+
name: String,
90+
},
8591
}
8692

8793
impl AgentSubcommand {
@@ -225,6 +231,33 @@ impl AgentSubcommand {
225231
style::SetAttribute(Attribute::Reset)
226232
)?;
227233
},
234+
Self::SetDefault { name } => match session.conversation.agents.agents.get(&name) {
235+
Some(agent) => {
236+
os.database
237+
.settings
238+
.set(Setting::ChatDefaultAgent, agent.name.clone())
239+
.await
240+
.map_err(|e| ChatError::Custom(e.to_string().into()))?;
241+
242+
execute!(
243+
session.stderr,
244+
style::SetForegroundColor(Color::Green),
245+
style::Print("✓ Default agent set to '"),
246+
style::Print(&agent.name),
247+
style::Print("'. This will take effect the next time q chat is launched.\n"),
248+
style::ResetColor,
249+
)?;
250+
},
251+
None => {
252+
execute!(
253+
session.stderr,
254+
style::SetForegroundColor(Color::Red),
255+
style::Print("Error: "),
256+
style::ResetColor,
257+
style::Print(format!("No agent with name {name} found\n")),
258+
)?;
259+
},
260+
},
228261
}
229262

230263
Ok(ChatState::PromptUser {
@@ -240,6 +273,7 @@ impl AgentSubcommand {
240273
Self::Set { .. } => "set",
241274
Self::Rename { .. } => "rename",
242275
Self::Schema => "schema",
276+
Self::SetDefault { .. } => "set_default",
243277
}
244278
}
245279
}

docs/agent-format.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Every agent configuration file can include the following sections:
1818

1919
## Name Field
2020

21-
The `name` field specifies the name of the agent. This is used for identification and display purposes. If not specified, the name is derived from the filename (without the `.json` extension).
21+
The `name` field specifies the name of the agent. This is used for identification and display purposes.
2222

2323
```json
2424
{
@@ -28,6 +28,17 @@ The `name` field specifies the name of the agent. This is used for identificatio
2828

2929
Note: While this field can be included in the configuration file, it will be overridden by the filename when the agent is loaded.
3030

31+
## Version Field
32+
33+
The `version` field specifies the version of the agent config. This is currently not being utilized though you must specify it.
34+
Currently, the default version is "0.1.0"
35+
36+
```json
37+
{
38+
"version": "0.1.0"
39+
}
40+
```
41+
3142
## Description Field
3243

3344
The `description` field provides a description of what the agent does. This is primarily for human readability and helps users distinguish between different agents.

0 commit comments

Comments
 (0)