Skip to content

Commit 4f5d017

Browse files
committed
merge with main
2 parents e746f9f + 6ed98e3 commit 4f5d017

40 files changed

+1279
-392
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
schemas/ @chaynabors

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = ["Amazon Q CLI Team ([email protected])", "Chay Nabors (nabochay@amazon
88
edition = "2024"
99
homepage = "https://aws.amazon.com/q/"
1010
publish = false
11-
version = "1.12.4"
11+
version = "1.13.2"
1212
license = "MIT OR Apache-2.0"
1313

1414
[workspace.dependencies]
@@ -92,7 +92,7 @@ security-framework = "3.2.0"
9292
semantic_search_client = { path = "crates/semantic-search-client" }
9393
semver = { version = "1.0.26", features = ["serde"] }
9494
serde = { version = "1.0.219", features = ["derive", "rc"] }
95-
serde_json = "1.0.140"
95+
serde_json = { version = "1.0.140", features = ["preserve_order"] }
9696
sha2 = "0.10.9"
9797
shell-color = "1.0.0"
9898
shell-words = "1.1.0"

crates/chat-cli/src/auth/builder_id.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@ impl BuilderIdToken {
303303

304304
/// Load the token from the keychain, refresh the token if it is expired and return it
305305
pub async fn load(database: &Database) -> Result<Option<Self>, AuthError> {
306+
// Can't use #[cfg(test)] without breaking lints, and we don't want to require
307+
// authentication in order to run ChatSession tests. Hence, adding this here with cfg!(test)
308+
if cfg!(test) {
309+
return Ok(Some(Self {
310+
access_token: Secret("test_access_token".to_string()),
311+
expires_at: time::OffsetDateTime::now_utc() + time::Duration::minutes(60),
312+
refresh_token: Some(Secret("test_refresh_token".to_string())),
313+
region: Some(OIDC_BUILDER_ID_REGION.to_string()),
314+
start_url: Some(START_URL.to_string()),
315+
oauth_flow: OAuthFlow::DeviceCode,
316+
scopes: Some(SCOPES.iter().map(|s| (*s).to_owned()).collect()),
317+
}));
318+
}
319+
306320
trace!("loading builder id token from the secret store");
307321
match database.get_secret(Self::SECRET_KEY).await {
308322
Ok(Some(secret)) => {

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ impl Display for HookTrigger {
3232
}
3333
}
3434

35+
#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Hash)]
36+
pub enum Source {
37+
Agent,
38+
Session,
39+
}
40+
41+
impl Default for Source {
42+
fn default() -> Self {
43+
Self::Agent
44+
}
45+
}
46+
3547
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, JsonSchema, Hash)]
3648
pub struct Hook {
3749
/// The command to run when the hook is triggered
@@ -48,15 +60,20 @@ pub struct Hook {
4860
/// How long the hook output is cached before it will be executed again
4961
#[serde(default = "Hook::default_cache_ttl_seconds")]
5062
pub cache_ttl_seconds: u64,
63+
64+
#[schemars(skip)]
65+
#[serde(default, skip_serializing)]
66+
pub source: Source,
5167
}
5268

5369
impl Hook {
54-
pub fn new(command: String) -> Self {
70+
pub fn new(command: String, source: Source) -> Self {
5571
Self {
5672
command,
5773
timeout_ms: Self::default_timeout_ms(),
5874
max_output_size: Self::default_max_output_size(),
5975
cache_ttl_seconds: Self::default_cache_ttl_seconds(),
76+
source,
6077
}
6178
}
6279

crates/chat-cli/src/cli/agent/legacy/hooks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ impl From<LegacyHook> for Option<Hook> {
8080
timeout_ms: value.timeout_ms,
8181
max_output_size: value.max_output_size,
8282
cache_ttl_seconds: value.cache_ttl_seconds,
83+
source: Default::default(),
8384
})
8485
}
8586
}

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ use crate::util::directories;
2525
///
2626
/// Returns [Some] with the newly migrated agents if the migration was performed, [None] if the
2727
/// migration was already done previously.
28-
pub async fn migrate(os: &mut Os) -> eyre::Result<Option<Vec<Agent>>> {
28+
pub async fn migrate(os: &mut Os, force: bool) -> eyre::Result<Option<Vec<Agent>>> {
2929
let has_migrated = os.database.get_has_migrated()?;
30-
if has_migrated.is_some_and(|has_migrated| has_migrated) {
30+
if !force && has_migrated.is_some_and(|has_migrated| has_migrated) {
3131
return Ok(None);
3232
}
3333

@@ -102,22 +102,27 @@ pub async fn migrate(os: &mut Os) -> eyre::Result<Option<Vec<Agent>>> {
102102
}
103103

104104
let labels = vec!["Yes", "No"];
105-
let selection: Option<_> = match Select::with_theme(&crate::util::dialoguer_theme())
106-
.with_prompt("Legacy profiles detected. Would you like to migrate them?")
107-
.items(&labels)
108-
.default(1)
109-
.interact_on_opt(&dialoguer::console::Term::stdout())
110-
{
111-
Ok(sel) => {
112-
let _ = crossterm::execute!(
113-
std::io::stdout(),
114-
crossterm::style::SetForegroundColor(crossterm::style::Color::Magenta)
115-
);
116-
sel
117-
},
118-
// Ctrl‑C -> Err(Interrupted)
119-
Err(dialoguer::Error::IO(ref e)) if e.kind() == std::io::ErrorKind::Interrupted => None,
120-
Err(e) => bail!("Failed to choose an option: {e}"),
105+
let selection: Option<_> = if !force {
106+
match Select::with_theme(&crate::util::dialoguer_theme())
107+
.with_prompt("Legacy profiles detected. Would you like to migrate them?")
108+
.items(&labels)
109+
.default(1)
110+
.interact_on_opt(&dialoguer::console::Term::stdout())
111+
{
112+
Ok(sel) => {
113+
let _ = crossterm::execute!(
114+
std::io::stdout(),
115+
crossterm::style::SetForegroundColor(crossterm::style::Color::Magenta)
116+
);
117+
sel
118+
},
119+
// Ctrl‑C -> Err(Interrupted)
120+
Err(dialoguer::Error::IO(ref e)) if e.kind() == std::io::ErrorKind::Interrupted => None,
121+
Err(e) => bail!("Failed to choose an option: {e}"),
122+
}
123+
} else {
124+
// Yes
125+
Some(0)
121126
};
122127

123128
if selection.is_none() || selection == Some(1) {

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

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

74+
pub 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,14 +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 derived from the file name. Thus they are skipped for
124-
/// serializing
125-
#[serde(skip)]
123+
#[serde(rename = "$schema", default = "default_schema")]
124+
pub schema: String,
125+
/// Name of the agent
126126
pub name: String,
127127
/// This field is not model facing and is mostly here for users to discern between agents
128128
#[serde(default)]
129129
pub description: Option<String>,
130-
/// (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
131131
/// agent. This should be seen as the same category of context as a system prompt.
132132
#[serde(default)]
133133
pub prompt: Option<String>,
@@ -168,7 +168,8 @@ pub struct Agent {
168168
impl Default for Agent {
169169
fn default() -> Self {
170170
Self {
171-
name: "default".to_string(),
171+
schema: default_schema(),
172+
name: DEFAULT_AGENT_NAME.to_string(),
172173
description: Some("Default agent".to_string()),
173174
prompt: Default::default(),
174175
mcp_servers: Default::default(),
@@ -206,18 +207,10 @@ impl Agent {
206207

207208
/// This function mutates the agent to a state that is usable for runtime.
208209
/// Practically this means to convert some of the fields value to their usable counterpart.
209-
/// For example, we populate the agent with its file name, convert the mcp array to actual
210-
/// 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.
211211
fn thaw(&mut self, path: &Path, global_mcp_config: Option<&McpServerConfig>) -> Result<(), AgentConfigError> {
212212
let Self { mcp_servers, .. } = self;
213213

214-
let name = path
215-
.file_stem()
216-
.ok_or(AgentConfigError::MissingFilename)?
217-
.to_string_lossy()
218-
.to_string();
219-
220-
self.name = name.clone();
221214
self.path = Some(path.to_path_buf());
222215

223216
if let (true, Some(global_mcp_config)) = (self.use_legacy_mcp_json, global_mcp_config) {
@@ -259,7 +252,7 @@ impl Agent {
259252
pub async fn get_agent_by_name(os: &Os, agent_name: &str) -> eyre::Result<(Agent, PathBuf)> {
260253
let config_path: Result<PathBuf, PathBuf> = 'config: {
261254
// local first, and then fall back to looking at global
262-
let local_config_dir = directories::chat_local_agent_dir()?.join(format!("{agent_name}.json"));
255+
let local_config_dir = directories::chat_local_agent_dir(os)?.join(format!("{agent_name}.json"));
263256
if os.fs.exists(&local_config_dir) {
264257
break 'config Ok(local_config_dir);
265258
}
@@ -397,7 +390,7 @@ impl Agents {
397390
};
398391

399392
let new_agents = if !skip_migration {
400-
match legacy::migrate(os).await {
393+
match legacy::migrate(os, false).await {
401394
Ok(Some(new_agents)) => {
402395
let migrated_count = new_agents.len();
403396
info!(migrated_count, "Profile migration successful");
@@ -431,7 +424,7 @@ impl Agents {
431424
},
432425
}
433426

434-
let Ok(path) = directories::chat_local_agent_dir() else {
427+
let Ok(path) = directories::chat_local_agent_dir(os) else {
435428
break 'local Vec::<Agent>::new();
436429
};
437430
let Ok(files) = os.fs.read_dir(path).await else {
@@ -641,7 +634,7 @@ impl Agents {
641634
agent
642635
});
643636

644-
"default".to_string()
637+
DEFAULT_AGENT_NAME.to_string()
645638
};
646639

647640
let _ = output.flush();
@@ -707,7 +700,7 @@ impl Agents {
707700
// Here the tool names can take the following forms:
708701
// - @{server_name}{delimiter}{tool_name}
709702
// - native_tool_name
710-
name == tool_name
703+
name == tool_name && matches!(origin, &ToolOrigin::Native)
711704
|| name.strip_prefix("@").is_some_and(|remainder| {
712705
remainder
713706
.split_once(MCP_SERVER_TOOL_DELIMITER)
@@ -776,6 +769,10 @@ async fn load_agents_from_entries(
776769
res
777770
}
778771

772+
fn default_schema() -> String {
773+
"https://raw.githubusercontent.com/aws/amazon-q-developer-cli/refs/heads/main/schemas/agent-v1.json".into()
774+
}
775+
779776
#[cfg(test)]
780777
fn validate_agent_name(name: &str) -> eyre::Result<()> {
781778
// Check if name is empty
@@ -800,6 +797,7 @@ mod tests {
800797

801798
const INPUT: &str = r#"
802799
{
800+
"name": "some_agent",
803801
"description": "My developer agent is used for small development tasks like solving open issues.",
804802
"prompt": "You are a principal developer who uses multiple agents to accomplish difficult engineering tasks",
805803
"mcpServers": {
@@ -841,11 +839,12 @@ mod tests {
841839
assert!(collection.get_active().is_none());
842840

843841
let agent = Agent::default();
844-
collection.agents.insert("default".to_string(), agent);
845-
collection.active_idx = "default".to_string();
842+
let agent_name = agent.name.clone();
843+
collection.agents.insert(agent_name.clone(), agent);
844+
collection.active_idx = agent_name.clone();
846845

847846
assert!(collection.get_active().is_some());
848-
assert_eq!(collection.get_active().unwrap().name, "default");
847+
assert_eq!(collection.get_active().unwrap().name, agent_name);
849848
}
850849

851850
#[test]

0 commit comments

Comments
 (0)