feat(agents): advertise declared agent variables in agent__list_available
This commit is contained in:
4 files changed
+84
-10
No files matched your search
+1
-1
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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"));
|
||||||
|
|||||||
Reference in new issue
Block a user