feat: teammates flag enabling peer messaging between concurrent graph agent nodes

Agent nodes marked teammates: true whose sub-agents run concurrently (map fan-out branches or multiple flagged agent nodes in one super-step) now form a team: each member gets a pre-provisioned inbox in a messaging-only PeerRegistry before any branch starts, a teammate roster injected into its prompt, peer resolution in agent__send_message, and a peers section in agent__list_running. Registries are scoped to a single fan-out and carry no lifecycle semantics: outputs still flow exclusively through collect_into/state reducers, finished peers reject new messages, and nothing registers in a supervisor. Also hardens the agent-recursion depth guard to work without a supervisor and adds validator warnings for zero-valued orchestration limits.
This commit is contained in:
Dark-Alex-17 committed 2026-09-09 16:44:23 -06:00
1 parent 7e84804ed1
commit 47edaa35ff
11 files changed
+975 -45

No files matched your search

+73
View File
@@ -176,6 +176,7 @@ impl GraphValidator {
self.validate_llm_nodes(graph, &mut result);
self.validate_llm_skills(graph, &mut result);
self.validate_max_concurrency(graph, &mut result);
self.validate_orchestration_limits(graph, &mut result);
self.validate_map_branches(graph, &mut result);
self.validate_parallel_user_interaction(graph, &mut result);
self.validate_parallel_writes(graph, &mut result);
@@ -529,6 +530,23 @@ impl GraphValidator {
}
}
fn validate_orchestration_limits(&self, graph: &Graph, result: &mut ValidationResult) {
if graph.max_agent_depth == Some(0) {
result.warning(ValidationError::new(
"`max_agent_depth: 0` forbids spawning any sub-agent, so every \
`agent` node and dynamic spawn will fail; remove the key or \
set it >= 1",
));
}
if graph.max_concurrent_agents == Some(0) {
result.warning(ValidationError::new(
"`max_concurrent_agents: 0` leaves no slot for any spawn to \
acquire, so agent spawning deadlocks; remove the key or set \
it >= 1",
));
}
}
fn validate_map_branches(&self, graph: &Graph, result: &mut ValidationResult) {
for (map_id, node) in &graph.nodes {
let NodeType::Map(m) = &node.node_type else {
@@ -1027,6 +1045,9 @@ mod tests {
top_p: None,
reasoning_effort: None,
max_concurrent_jobs: None,
can_spawn_agents: None,
max_concurrent_agents: None,
max_agent_depth: None,
global_tools: Vec::new(),
mcp_servers: Vec::new(),
mcp_tools: None,
@@ -1637,6 +1658,7 @@ mod tests {
state_updates: None,
output_schema: None,
timeout: None,
teammates: false,
}),
next: next.map(NextTargets::from),
}
@@ -2596,6 +2618,57 @@ mod tests {
);
}
#[test]
fn zero_max_agent_depth_warns() {
let mut graph = graph_with(vec![("e", end_node("e"))], "e");
graph.max_agent_depth = Some(0);
let result = validator().validate(&graph);
assert!(result.is_valid());
assert!(
result
.warnings
.iter()
.any(|w| w.message.contains("`max_agent_depth: 0`")),
"expected max_agent_depth=0 warning: {:?}",
result.warnings
);
}
#[test]
fn zero_max_concurrent_agents_warns() {
let mut graph = graph_with(vec![("e", end_node("e"))], "e");
graph.max_concurrent_agents = Some(0);
let result = validator().validate(&graph);
assert!(result.is_valid());
assert!(
result
.warnings
.iter()
.any(|w| w.message.contains("`max_concurrent_agents: 0`")),
"expected max_concurrent_agents=0 warning: {:?}",
result.warnings
);
}
#[test]
fn unset_orchestration_limits_do_not_warn() {
let graph = graph_with(vec![("e", end_node("e"))], "e");
let result = validator().validate(&graph);
assert!(
!result.warnings.iter().any(|w| {
w.message.contains("max_agent_depth") || w.message.contains("max_concurrent_agents")
}),
"unset limits should not warn: {:?}",
result.warnings
);
}
#[test]
fn map_max_concurrency_zero_errors() {
let mut map = map_node_basic("m", "br", Some("end"));