feat: support explicit can_spawn_agents, max_concurrent_agents, and max_agent_depth keys in graph.yaml
Graph agents previously derived can_spawn_agents from the presence of an agent node, so a pure-LLM-node graph could never spawn dynamic sub-agents. The three optional graph-level keys now override the derivation/defaults in AgentConfig::from_graph, enabling orchestrator graphs (can_spawn_agents: true with only llm nodes). Explicit false on a graph with agent nodes still runs those nodes; only dynamic spawning from llm nodes is disabled.
This commit is contained in:
3 files changed
+115
-5
No files matched your search
+12
-1
@@ -38,12 +38,23 @@ reasoning_effort: null # Default reasoning effort for `llm` nodes th
|
|||||||
|
|
||||||
max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once across the
|
max_concurrent_jobs: 5 # Max background jobs (`job__*` tools) running at once across the
|
||||||
# whole graph run: every `llm` node (including parallel branches)
|
# whole graph run: every `llm` node (including parallel branches)
|
||||||
# draws from this one pool, so the budget is graph-wide — there is
|
# draws from this one pool, so the budget is graph-wide. There is
|
||||||
# no per-node override. Jobs themselves are node-local: the node
|
# no per-node override. Jobs themselves are node-local: the node
|
||||||
# that starts a job must collect or cancel it before it ends, and
|
# that starts a job must collect or cancel it before it ends, and
|
||||||
# anything left running at node exit is cancelled. Overrides the
|
# anything left running at node exit is cancelled. Overrides the
|
||||||
# global setting; 0 disables background jobs for this graph agent.
|
# global setting; 0 disables background jobs for this graph agent.
|
||||||
|
|
||||||
|
can_spawn_agents: true # Whether `llm` nodes may spawn dynamic sub-agents via the
|
||||||
|
# agent__* tools. Defaults to true iff the graph has an `agent`
|
||||||
|
# node; an explicit value overrides that. Set it true on a graph
|
||||||
|
# with only `llm` nodes to build an orchestrator graph. Setting
|
||||||
|
# it false on a graph with `agent` nodes still runs those nodes,
|
||||||
|
# only dynamic spawning from `llm` nodes is disabled.
|
||||||
|
max_concurrent_agents: 4 # Max dynamically spawned sub-agents running at once. Explicit
|
||||||
|
# value overrides the default of 4.
|
||||||
|
max_agent_depth: 3 # Max agent nesting depth (agents spawning agents). Explicit
|
||||||
|
# value overrides the default of 3.
|
||||||
|
|
||||||
global_tools: # Tool universe an `llm` node's `tools:` whitelist draws from
|
global_tools: # Tool universe an `llm` node's `tools:` whitelist draws from
|
||||||
- web_search_coyote.sh
|
- web_search_coyote.sh
|
||||||
- fetch_url_via_curl.sh
|
- fetch_url_via_curl.sh
|
||||||
|
|||||||
+71
-4
@@ -835,7 +835,7 @@ fn default_max_concurrent_agents() -> usize {
|
|||||||
4
|
4
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_max_agent_depth() -> usize {
|
pub(crate) fn default_max_agent_depth() -> usize {
|
||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,10 +878,16 @@ impl AgentConfig {
|
|||||||
skill_instructions: graph.skill_instructions.clone(),
|
skill_instructions: graph.skill_instructions.clone(),
|
||||||
conversation_starters: graph.conversation_starters.clone(),
|
conversation_starters: graph.conversation_starters.clone(),
|
||||||
variables: graph.variables.clone(),
|
variables: graph.variables.clone(),
|
||||||
can_spawn_agents: graph.has_agent_node(),
|
can_spawn_agents: graph
|
||||||
max_concurrent_agents: default_max_concurrent_agents(),
|
.can_spawn_agents
|
||||||
|
.unwrap_or_else(|| graph.has_agent_node()),
|
||||||
|
max_concurrent_agents: graph
|
||||||
|
.max_concurrent_agents
|
||||||
|
.unwrap_or_else(default_max_concurrent_agents),
|
||||||
max_concurrent_jobs: graph.max_concurrent_jobs,
|
max_concurrent_jobs: graph.max_concurrent_jobs,
|
||||||
max_agent_depth: default_max_agent_depth(),
|
max_agent_depth: graph
|
||||||
|
.max_agent_depth
|
||||||
|
.unwrap_or_else(default_max_agent_depth),
|
||||||
escalation_timeout: default_escalation_timeout(),
|
escalation_timeout: default_escalation_timeout(),
|
||||||
..AgentConfig::default()
|
..AgentConfig::default()
|
||||||
}
|
}
|
||||||
@@ -1460,6 +1466,67 @@ variables:
|
|||||||
assert_eq!(config.escalation_timeout, default_escalation_timeout());
|
assert_eq!(config.escalation_timeout, default_escalation_timeout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_graph_explicit_can_spawn_agents_enables_spawning_without_agent_nodes() {
|
||||||
|
let yaml = formatdoc! {r#"
|
||||||
|
name: g
|
||||||
|
can_spawn_agents: true
|
||||||
|
start: x
|
||||||
|
nodes:
|
||||||
|
x:
|
||||||
|
id: x
|
||||||
|
type: end
|
||||||
|
output: ok
|
||||||
|
"#};
|
||||||
|
let graph: Graph = serde_yaml::from_str(&yaml).unwrap();
|
||||||
|
|
||||||
|
assert!(AgentConfig::from_graph("d", &graph).can_spawn_agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_graph_explicit_can_spawn_agents_false_wins_over_agent_nodes() {
|
||||||
|
let yaml = formatdoc! {r#"
|
||||||
|
name: g
|
||||||
|
can_spawn_agents: false
|
||||||
|
start: a
|
||||||
|
nodes:
|
||||||
|
a:
|
||||||
|
id: a
|
||||||
|
type: agent
|
||||||
|
agent: helper
|
||||||
|
prompt: hi
|
||||||
|
next: e
|
||||||
|
e:
|
||||||
|
id: e
|
||||||
|
type: end
|
||||||
|
output: done
|
||||||
|
"#};
|
||||||
|
let graph: Graph = serde_yaml::from_str(&yaml).unwrap();
|
||||||
|
|
||||||
|
assert!(!AgentConfig::from_graph("d", &graph).can_spawn_agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_graph_propagates_explicit_agent_limits() {
|
||||||
|
let yaml = formatdoc! {r#"
|
||||||
|
name: g
|
||||||
|
max_concurrent_agents: 7
|
||||||
|
max_agent_depth: 3
|
||||||
|
start: x
|
||||||
|
nodes:
|
||||||
|
x:
|
||||||
|
id: x
|
||||||
|
type: end
|
||||||
|
output: ok
|
||||||
|
"#};
|
||||||
|
let graph: Graph = serde_yaml::from_str(&yaml).unwrap();
|
||||||
|
|
||||||
|
let config = AgentConfig::from_graph("d", &graph);
|
||||||
|
|
||||||
|
assert_eq!(config.max_concurrent_agents, 7);
|
||||||
|
assert_eq!(config.max_agent_depth, 3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_metadata_stub_extracts_description_from_graph_yaml() {
|
fn agent_metadata_stub_extracts_description_from_graph_yaml() {
|
||||||
let yaml = r#"
|
let yaml = r#"
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ pub struct Graph {
|
|||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub max_concurrent_jobs: Option<usize>,
|
pub max_concurrent_jobs: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub can_spawn_agents: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_concurrent_agents: Option<usize>,
|
||||||
|
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub max_agent_depth: Option<usize>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub global_tools: Vec<String>,
|
pub global_tools: Vec<String>,
|
||||||
|
|
||||||
@@ -946,11 +955,34 @@ nodes:
|
|||||||
assert!(graph.temperature.is_none());
|
assert!(graph.temperature.is_none());
|
||||||
assert!(graph.top_p.is_none());
|
assert!(graph.top_p.is_none());
|
||||||
assert!(graph.max_concurrent_jobs.is_none());
|
assert!(graph.max_concurrent_jobs.is_none());
|
||||||
|
assert!(graph.can_spawn_agents.is_none());
|
||||||
|
assert!(graph.max_concurrent_agents.is_none());
|
||||||
|
assert!(graph.max_agent_depth.is_none());
|
||||||
assert!(graph.global_tools.is_empty());
|
assert!(graph.global_tools.is_empty());
|
||||||
assert!(graph.mcp_servers.is_empty());
|
assert!(graph.mcp_servers.is_empty());
|
||||||
assert!(graph.conversation_starters.is_empty());
|
assert!(graph.conversation_starters.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn graph_parses_agent_orchestration_keys() {
|
||||||
|
let yaml = r#"
|
||||||
|
name: g
|
||||||
|
can_spawn_agents: true
|
||||||
|
max_concurrent_agents: 6
|
||||||
|
max_agent_depth: 2
|
||||||
|
start: x
|
||||||
|
nodes:
|
||||||
|
x:
|
||||||
|
id: x
|
||||||
|
type: end
|
||||||
|
output: ok
|
||||||
|
"#;
|
||||||
|
let graph: Graph = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
assert_eq!(graph.can_spawn_agents, Some(true));
|
||||||
|
assert_eq!(graph.max_concurrent_agents, Some(6));
|
||||||
|
assert_eq!(graph.max_agent_depth, Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn node_ids_lists_nodes_in_order() {
|
fn node_ids_lists_nodes_in_order() {
|
||||||
let yaml = r#"
|
let yaml = r#"
|
||||||
|
|||||||
Reference in new issue
Block a user