feat(agents): advertise declared agent variables in agent__list_available

This commit is contained in:
Dark-Alex-17 committed 2026-09-12 01:37:20 -06:00
1 parent eb7d1421da
commit 1ec7916816
4 files changed
+84 -10

No files matched your search

+1 -1
View File
@@ -1258,7 +1258,7 @@ fn load_agent_description(name: &str) -> String {
String::new() String::new()
} }
fn load_agent_variables(name: &str) -> Vec<AgentVariable> { pub fn load_agent_variables(name: &str) -> Vec<AgentVariable> {
if let Ok(config) = AgentConfig::load(&paths::agent_config_file(name)) { if let Ok(config) = AgentConfig::load(&paths::agent_config_file(name)) {
return config.variables; return config.variables;
} }
+1 -1
View File
@@ -28,7 +28,7 @@ pub(crate) use self::agent::AgentConfig;
pub(crate) use self::agent::default_max_agent_depth; pub(crate) use self::agent::default_max_agent_depth;
pub use self::agent::{ pub use self::agent::{
Agent, AgentVariable, AgentVariables, complete_agent_variables, list_agents, Agent, AgentVariable, AgentVariables, complete_agent_variables, list_agents,
list_agents_with_descriptions, list_agents_with_descriptions, load_agent_variables,
}; };
#[allow(unused_imports)] #[allow(unused_imports)]
pub use self::app_config::AppConfig; pub use self::app_config::AppConfig;
+1 -1
View File
@@ -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__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__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__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__list_running` | List all subagents YOU have spawned, with their status. |
| `agent__cancel` | Cancel a running agent by ID. | | `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. | | `agent__task_create` | Create a task in the dependency-aware task queue. Optional `variables` (string map) is passed to the auto-dispatched agent. |
+81 -7
View File
@@ -4,7 +4,7 @@ use crate::client::{Model, ModelType, call_chat_completions};
use crate::config::{ use crate::config::{
Agent, AgentVariable, AgentVariables, AppState, Input, RequestContext, Role, RoleLike, Agent, AgentVariable, AgentVariables, AppState, Input, RequestContext, Role, RoleLike,
default_max_agent_depth, effective_max_concurrent_jobs, jobs_enabled, 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::mailbox::{Envelope, EnvelopePayload, Inbox, PeerRegistry, graph_agent_id};
use crate::supervisor::notification::agent_notification; use crate::supervisor::notification::agent_notification;
@@ -337,7 +337,7 @@ pub fn agent_function_declarations() -> Vec<FunctionDeclaration> {
"variables".to_string(), "variables".to_string(),
JsonSchema { JsonSchema {
type_value: Some("object".to_string()), 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() ..Default::default()
}, },
), ),
@@ -404,9 +404,11 @@ pub fn agent_function_declarations() -> Vec<FunctionDeclaration> {
}, },
FunctionDeclaration { FunctionDeclaration {
name: format!("{AGENT_FUNCTION_PREFIX}list_available"), name: format!("{AGENT_FUNCTION_PREFIX}list_available"),
description: "List all agent types installed and available to spawn (name + description). Use this to \ description: "List all agent types installed and available to spawn (name + description + declared \
discover what specialists exist before calling `agent__spawn` — especially when you're unsure \ variables). Use this to discover what specialists exist before calling `agent__spawn` — \
which agent to delegate to. This is the discovery counterpart to `agent__list_running` \ 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(), (which reports agents you have already spawned).".to_string(),
parameters: JsonSchema { parameters: JsonSchema {
type_value: Some("object".to_string()), type_value: Some("object".to_string()),
@@ -490,7 +492,7 @@ pub fn agent_function_declarations() -> Vec<FunctionDeclaration> {
"variables".to_string(), "variables".to_string(),
JsonSchema { JsonSchema {
type_value: Some("object".to_string()), 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() ..Default::default()
}, },
), ),
@@ -1443,6 +1445,27 @@ fn handle_list_running(ctx: &mut RequestContext) -> Result<Value> {
Ok(result) Ok(result)
} }
fn variables_json(vars: &[AgentVariable]) -> Option<Value> {
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<Value> { fn handle_list_available(ctx: &RequestContext) -> Result<Value> {
let whitelist: Option<Vec<String>> = ctx let whitelist: Option<Vec<String>> = ctx
.agent .agent
@@ -1458,11 +1481,15 @@ fn handle_list_available(ctx: &RequestContext) -> Result<Value> {
let agents: Vec<Value> = entries let agents: Vec<Value> = entries
.into_iter() .into_iter()
.map(|(name, description)| { .map(|(name, description)| {
if description.is_empty() { let mut agent = if description.is_empty() {
json!({ "name": name }) json!({ "name": name })
} else { } else {
json!({ "name": name, "description": description }) json!({ "name": name, "description": description })
};
if let Some(variables) = variables_json(&load_agent_variables(&name)) {
agent["variables"] = variables;
} }
agent
}) })
.collect(); .collect();
@@ -2635,6 +2662,53 @@ mod tests {
assert_eq!(full_count as usize, list_agents_with_descriptions().len()); 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] #[test]
fn agent_permitted_none_whitelist_allows_all() { fn agent_permitted_none_whitelist_allows_all() {
assert!(agent_permitted(None, "explore")); assert!(agent_permitted(None, "explore"));