From 1ec79168167a6a9148f594b7f5f1283266dbc287 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 12 Sep 2026 00:26:14 -0600 Subject: [PATCH] feat(agents): advertise declared agent variables in agent__list_available --- src/config/agent.rs | 2 +- src/config/mod.rs | 2 +- src/config/prompts.rs | 2 +- src/function/agents.rs | 88 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/config/agent.rs b/src/config/agent.rs index 617c206..8951d1a 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -1258,7 +1258,7 @@ fn load_agent_description(name: &str) -> String { String::new() } -fn load_agent_variables(name: &str) -> Vec { +pub fn load_agent_variables(name: &str) -> Vec { if let Ok(config) = AgentConfig::load(&paths::agent_config_file(name)) { return config.variables; } diff --git a/src/config/mod.rs b/src/config/mod.rs index 6c909fd..efacc93 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -28,7 +28,7 @@ pub(crate) use self::agent::AgentConfig; pub(crate) use self::agent::default_max_agent_depth; pub use self::agent::{ Agent, AgentVariable, AgentVariables, complete_agent_variables, list_agents, - list_agents_with_descriptions, + list_agents_with_descriptions, load_agent_variables, }; #[allow(unused_imports)] pub use self::app_config::AppConfig; diff --git a/src/config/prompts.rs b/src/config/prompts.rs index ce3c077..4aeda0b 100644 --- a/src/config/prompts.rs +++ b/src/config/prompts.rs @@ -87,7 +87,7 @@ pub(in crate::config) const DEFAULT_SPAWN_INSTRUCTIONS: &str = indoc! {" | `agent__spawn` | Spawn a subagent in the background. Returns an `id` immediately. Optional `variables` (string map) sets the target agent's declared variables, overriding inherited values. | | `agent__check` | Non-blocking status probe: running or finished. Never returns/consumes the result — use `agent__collect`. | | `agent__collect` | Blocking wait: wait for an agent to finish, return its output. | - | `agent__list_available` | List all agent types you can spawn (name + description). Use this to discover specialists before calling `agent__spawn`. | + | `agent__list_available` | List all agent types you can spawn (name + description + declared variables). Use this to discover specialists and the `variables` they accept before calling `agent__spawn`. | | `agent__list_running` | List all subagents YOU have spawned, with their status. | | `agent__cancel` | Cancel a running agent by ID. | | `agent__task_create` | Create a task in the dependency-aware task queue. Optional `variables` (string map) is passed to the auto-dispatched agent. | diff --git a/src/function/agents.rs b/src/function/agents.rs index b9d1cd9..9d30c97 100644 --- a/src/function/agents.rs +++ b/src/function/agents.rs @@ -4,7 +4,7 @@ use crate::client::{Model, ModelType, call_chat_completions}; use crate::config::{ Agent, AgentVariable, AgentVariables, AppState, Input, RequestContext, Role, RoleLike, default_max_agent_depth, effective_max_concurrent_jobs, jobs_enabled, - list_agents_with_descriptions, + list_agents_with_descriptions, load_agent_variables, }; use crate::supervisor::mailbox::{Envelope, EnvelopePayload, Inbox, PeerRegistry, graph_agent_id}; use crate::supervisor::notification::agent_notification; @@ -337,7 +337,7 @@ pub fn agent_function_declarations() -> Vec { "variables".to_string(), JsonSchema { type_value: Some("object".to_string()), - description: Some("Values for the target agent's declared variables (its config/graph `variables:` list), e.g. {\"pr\": \"1234\"}. Overrides values inherited from the parent. String values only. Note: if the target agent resumes an existing shared session, the session's stored variable values take precedence and these are ignored.".into()), + description: Some("Values for the target agent's declared variables (its config/graph `variables:` list), e.g. {\"pr\": \"1234\"}. Overrides values inherited from the parent. String values only. Note: if the target agent resumes an existing shared session, the session's stored variable values take precedence and these are ignored. Discover an agent's declared variables via `agent__list_available`.".into()), ..Default::default() }, ), @@ -404,9 +404,11 @@ pub fn agent_function_declarations() -> Vec { }, FunctionDeclaration { name: format!("{AGENT_FUNCTION_PREFIX}list_available"), - description: "List all agent types installed and available to spawn (name + description). Use this to \ - discover what specialists exist before calling `agent__spawn` — especially when you're unsure \ - which agent to delegate to. This is the discovery counterpart to `agent__list_running` \ + description: "List all agent types installed and available to spawn (name + description + declared \ + variables). Use this to discover what specialists exist before calling `agent__spawn` — \ + especially when you're unsure which agent to delegate to. Each agent's `variables` entry \ + describes the values to pass via the `variables` parameter of `agent__spawn` or \ + `agent__task_create`. This is the discovery counterpart to `agent__list_running` \ (which reports agents you have already spawned).".to_string(), parameters: JsonSchema { type_value: Some("object".to_string()), @@ -490,7 +492,7 @@ pub fn agent_function_declarations() -> Vec { "variables".to_string(), JsonSchema { type_value: Some("object".to_string()), - description: Some("Values for the auto-spawned agent's declared variables, e.g. {\"pr\": \"1234\"}. Passed to `agent__spawn` at dispatch time. String values only.".into()), + description: Some("Values for the auto-spawned agent's declared variables, e.g. {\"pr\": \"1234\"}. Passed to `agent__spawn` at dispatch time. String values only. Discover an agent's declared variables via `agent__list_available`.".into()), ..Default::default() }, ), @@ -1443,6 +1445,27 @@ fn handle_list_running(ctx: &mut RequestContext) -> Result { Ok(result) } +fn variables_json(vars: &[AgentVariable]) -> Option { + if vars.is_empty() { + return None; + } + Some(Value::Array( + vars.iter() + .map(|v| { + let mut var = json!({ + "name": v.name, + "description": v.description, + "required": v.default.is_none(), + }); + if let Some(default) = &v.default { + var["default"] = json!(default); + } + var + }) + .collect(), + )) +} + fn handle_list_available(ctx: &RequestContext) -> Result { let whitelist: Option> = ctx .agent @@ -1458,11 +1481,15 @@ fn handle_list_available(ctx: &RequestContext) -> Result { let agents: Vec = entries .into_iter() .map(|(name, description)| { - if description.is_empty() { + let mut agent = if description.is_empty() { json!({ "name": name }) } else { json!({ "name": name, "description": description }) + }; + if let Some(variables) = variables_json(&load_agent_variables(&name)) { + agent["variables"] = variables; } + agent }) .collect(); @@ -2635,6 +2662,53 @@ mod tests { assert_eq!(full_count as usize, list_agents_with_descriptions().len()); } + #[test] + fn variables_json_maps_required_and_default() { + let vars = vec![ + AgentVariable { + name: "pr".to_string(), + description: "PR number".to_string(), + default: None, + value: String::new(), + }, + AgentVariable { + name: "branch".to_string(), + description: "Target branch".to_string(), + default: Some("x".to_string()), + value: String::new(), + }, + ]; + + let result = variables_json(&vars).unwrap(); + + assert_eq!(result[0]["name"], "pr"); + assert_eq!(result[0]["description"], "PR number"); + assert_eq!(result[0]["required"], true); + assert!(result[0].get("default").is_none()); + assert_eq!(result[1]["name"], "branch"); + assert_eq!(result[1]["required"], false); + assert_eq!(result[1]["default"], "x"); + } + + #[test] + fn variables_json_empty_slice_is_none() { + assert!(variables_json(&[]).is_none()); + } + + #[test] + fn variables_json_never_emits_value() { + let vars = vec![AgentVariable { + name: "pr".to_string(), + description: "PR number".to_string(), + default: None, + value: "resolved".to_string(), + }]; + + let result = variables_json(&vars).unwrap(); + + assert!(result[0].get("value").is_none()); + } + #[test] fn agent_permitted_none_whitelist_allows_all() { assert!(agent_permitted(None, "explore"));