Add named agent nickname packs

This commit is contained in:
Nick Baumann
2026-03-18 22:59:47 -07:00
parent 7014bebfed
commit fa9af900e1
3 changed files with 143 additions and 13 deletions
+30 -4
View File
@@ -6,6 +6,19 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AgentNicknameCandidatesToml": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
]
},
"AgentRoleToml": {
"additionalProperties": false,
"properties": {
@@ -55,11 +68,24 @@
"type": "integer"
},
"nickname_candidates": {
"description": "Global fallback nickname candidates for spawned agent threads.\n\nExample: ```toml [agents] nickname_candidates = [\"Scout\", \"Builder\", \"Reviewer\"] ```",
"items": {
"type": "string"
"allOf": [
{
"$ref": "#/definitions/AgentNicknameCandidatesToml"
}
],
"default": null,
"description": "Global fallback nickname candidates for spawned agent threads.\n\nExample: ```toml [agents] nickname_candidates = [\"Scout\", \"Builder\", \"Reviewer\"]\n\n# Or select a named pack: nickname_candidates = \"succession\"\n\n[agents.nickname_packs] succession = [\"Shiv\", \"Roman\", \"Kendall\"] ```"
},
"nickname_packs": {
"additionalProperties": {
"items": {
"type": "string"
},
"type": "array"
},
"type": "array"
"default": {},
"description": "Named global fallback nickname candidate packs keyed by pack name.",
"type": "object"
}
},
"type": "object"
+47 -2
View File
@@ -118,11 +118,11 @@ nickname_candidates = ["Scout", "Builder", "Reviewer"]
agents_with_nickname_candidates_cfg
.agents
.and_then(|agents| agents.nickname_candidates),
Some(vec![
Some(AgentNicknameCandidatesToml::Candidates(vec![
"Scout".to_string(),
"Builder".to_string(),
"Reviewer".to_string()
])
]))
);
let memories = r#"
@@ -1704,6 +1704,46 @@ trust_level = "trusted"
Ok(())
}
#[tokio::test]
async fn selected_agents_nickname_pack_is_used() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let workspace = TempDir::new()?;
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
format!(
r#"
[agents]
nickname_candidates = "succession"
[agents.nickname_packs]
succession = ["Shiv", " Roman ", "", "Kendall", "Shiv"]
the_office = ["Pam", "Jim"]
[projects."{workspace_key}"]
trust_level = "trusted"
"#
),
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(workspace.path().to_path_buf()))
.build()
.await?;
assert_eq!(
config.agent_nickname_candidates,
vec![
"Shiv".to_string(),
"Roman".to_string(),
"Kendall".to_string()
]
);
Ok(())
}
#[test]
fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -3190,6 +3230,7 @@ fn load_config_rejects_missing_agent_role_config_file() -> std::io::Result<()> {
max_depth: None,
job_max_runtime_seconds: None,
nickname_candidates: None,
nickname_packs: BTreeMap::new(),
roles: BTreeMap::from([(
"researcher".to_string(),
AgentRoleToml {
@@ -4056,6 +4097,7 @@ fn load_config_normalizes_agent_role_nickname_candidates() -> std::io::Result<()
max_depth: None,
job_max_runtime_seconds: None,
nickname_candidates: None,
nickname_packs: BTreeMap::new(),
roles: BTreeMap::from([(
"researcher".to_string(),
AgentRoleToml {
@@ -4098,6 +4140,7 @@ fn load_config_rejects_empty_agent_role_nickname_candidates() -> std::io::Result
max_depth: None,
job_max_runtime_seconds: None,
nickname_candidates: None,
nickname_packs: BTreeMap::new(),
roles: BTreeMap::from([(
"researcher".to_string(),
AgentRoleToml {
@@ -4134,6 +4177,7 @@ fn load_config_rejects_duplicate_agent_role_nickname_candidates() -> std::io::Re
max_depth: None,
job_max_runtime_seconds: None,
nickname_candidates: None,
nickname_packs: BTreeMap::new(),
roles: BTreeMap::from([(
"researcher".to_string(),
AgentRoleToml {
@@ -4170,6 +4214,7 @@ fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Resul
max_depth: None,
job_max_runtime_seconds: None,
nickname_candidates: None,
nickname_packs: BTreeMap::new(),
roles: BTreeMap::from([(
"researcher".to_string(),
AgentRoleToml {
+66 -7
View File
@@ -1675,8 +1675,22 @@ pub struct AgentsToml {
/// ```toml
/// [agents]
/// nickname_candidates = ["Scout", "Builder", "Reviewer"]
///
/// # Or select a named pack:
/// nickname_candidates = "succession"
///
/// [agents.nickname_packs]
/// succession = ["Shiv", "Roman", "Kendall"]
/// ```
pub nickname_candidates: Option<Vec<String>>,
#[serde(
default,
deserialize_with = "deserialize_optional_agent_nickname_candidates"
)]
pub nickname_candidates: Option<AgentNicknameCandidatesToml>,
/// Named global fallback nickname candidate packs keyed by pack name.
#[serde(default)]
pub nickname_packs: BTreeMap<String, Vec<String>>,
/// User-defined role declarations keyed by role name.
///
@@ -1691,6 +1705,22 @@ pub struct AgentsToml {
pub roles: BTreeMap<String, AgentRoleToml>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
#[serde(untagged)]
pub enum AgentNicknameCandidatesToml {
Candidates(Vec<String>),
Pack(String),
}
fn deserialize_optional_agent_nickname_candidates<'de, D>(
deserializer: D,
) -> Result<Option<AgentNicknameCandidatesToml>, D::Error>
where
D: Deserializer<'de>,
{
Option::<AgentNicknameCandidatesToml>::deserialize(deserializer)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AgentRoleConfig {
/// Human-facing role documentation used in spawn tool guidance.
@@ -2454,12 +2484,41 @@ impl Config {
"agents.job_max_runtime_seconds must fit within a 64-bit signed integer",
));
}
let agent_nickname_candidates = agent_roles::normalize_global_agent_nickname_candidates(
"agents.nickname_candidates",
cfg.agents
.as_ref()
.and_then(|agents| agents.nickname_candidates.as_deref()),
)?
let agent_nickname_candidates = {
let candidates = match cfg.agents.as_ref().and_then(|agents| {
agents.nickname_candidates.as_ref().map(|nickname_candidates| {
(nickname_candidates, &agents.nickname_packs)
})
}) {
Some((AgentNicknameCandidatesToml::Candidates(candidates), _)) => {
Some(candidates.as_slice())
}
Some((AgentNicknameCandidatesToml::Pack(pack), nickname_packs)) => {
let selected_pack = pack.trim();
if selected_pack.is_empty() {
None
} else {
match nickname_packs.get(selected_pack) {
Some(candidates) => Some(candidates.as_slice()),
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"agents.nickname_candidates references unknown nickname pack `{selected_pack}`"
),
));
}
}
}
}
None => None,
};
agent_roles::normalize_global_agent_nickname_candidates(
"agents.nickname_candidates",
candidates,
)?
}
.unwrap_or_default();
let background_terminal_max_timeout = cfg
.background_terminal_max_timeout