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:
11 files changed
+975
-45
No files matched your search
@@ -25,6 +25,7 @@ mod update;
|
||||
|
||||
#[cfg(test)]
|
||||
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,
|
||||
|
||||
@@ -215,7 +215,7 @@ pub(in crate::config) const DEFAULT_TEAMMATE_INSTRUCTIONS: &str = indoc! {"
|
||||
## Teammate Messaging
|
||||
|
||||
You have tools to communicate with other agents running alongside you:
|
||||
- `agent__send_message --id <agent_id> --message \"...\"`: Send a message to a child or sibling agent by its id, or to your parent with the reserved id `parent`.
|
||||
- `agent__send_message --id <agent_id> --message \"...\"`: Send a message to a child, sibling, or concurrent teammate agent by its id, or to your parent with the reserved id `parent`.
|
||||
- `agent__check_inbox`: Check for messages sent to you by other agents.
|
||||
|
||||
If you are working alongside other agents (e.g. reviewing different files, exploring different areas):
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::mcp::{
|
||||
use crate::rag::Rag;
|
||||
use crate::supervisor::Supervisor;
|
||||
use crate::supervisor::escalation::EscalationQueue;
|
||||
use crate::supervisor::mailbox::Inbox;
|
||||
use crate::supervisor::mailbox::{Inbox, PeerRegistry};
|
||||
use crate::supervisor::notification::NotificationQueue;
|
||||
use crate::utils::{
|
||||
AbortSignal, abortable_run_with_spinner, edit_file, fuzzy_filter, get_env_name,
|
||||
@@ -328,6 +328,13 @@ pub struct RequestContext {
|
||||
pub self_agent_id: Option<String>,
|
||||
pub inbox: Option<Arc<Inbox>>,
|
||||
pub parent_inbox: Option<Arc<Inbox>>,
|
||||
/// Directory of concurrent teammates for `teammates: true` graph fan-outs.
|
||||
/// Never inherited: fork sites and `run_agent_for_graph` propagate it
|
||||
/// explicitly so it cannot leak into nested fan-outs or spawned children.
|
||||
pub peer_registry: Option<Arc<PeerRegistry>>,
|
||||
/// Pre-provisioned (agent id, inbox) for the sub-agent this branch context
|
||||
/// will run, consumed by `run_agent_for_graph`.
|
||||
pub peer_assignment: Option<(String, Arc<Inbox>)>,
|
||||
pub escalation_queue: Option<Arc<EscalationQueue>>,
|
||||
pub notification_queue: Arc<NotificationQueue>,
|
||||
pub current_depth: usize,
|
||||
@@ -369,6 +376,8 @@ impl RequestContext {
|
||||
self_agent_id: None,
|
||||
inbox: None,
|
||||
parent_inbox: None,
|
||||
peer_registry: None,
|
||||
peer_assignment: None,
|
||||
escalation_queue: None,
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth: 0,
|
||||
@@ -435,6 +444,8 @@ impl RequestContext {
|
||||
self_agent_id: None,
|
||||
inbox: None,
|
||||
parent_inbox: None,
|
||||
peer_registry: None,
|
||||
peer_assignment: None,
|
||||
escalation_queue: None,
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth: 0,
|
||||
@@ -490,6 +501,8 @@ impl RequestContext {
|
||||
self_agent_id: self.self_agent_id.clone(),
|
||||
inbox: self.inbox.clone(),
|
||||
parent_inbox: self.parent_inbox.clone(),
|
||||
peer_registry: None,
|
||||
peer_assignment: None,
|
||||
escalation_queue: self.escalation_queue.clone(),
|
||||
notification_queue: self.notification_queue.clone(),
|
||||
current_depth: self.current_depth,
|
||||
@@ -541,6 +554,8 @@ impl RequestContext {
|
||||
self_agent_id: Some(self_agent_id),
|
||||
inbox: Some(inbox),
|
||||
parent_inbox: parent.inbox.clone(),
|
||||
peer_registry: None,
|
||||
peer_assignment: None,
|
||||
escalation_queue: parent.escalation_queue.clone(),
|
||||
notification_queue: Arc::new(NotificationQueue::new()),
|
||||
current_depth,
|
||||
@@ -6040,6 +6055,40 @@ mod tests {
|
||||
assert!(Arc::ptr_eq(&parent_inbox, &child_parent_inbox));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_for_branch_resets_peer_fields() {
|
||||
let mut ctx = create_test_ctx();
|
||||
ctx.peer_registry = Some(Arc::new(PeerRegistry::new()));
|
||||
ctx.peer_assignment = Some(("graph_agent_a_1".to_string(), Arc::new(Inbox::new())));
|
||||
|
||||
let branch = ctx.fork_for_branch();
|
||||
|
||||
assert!(
|
||||
branch.peer_registry.is_none() && branch.peer_assignment.is_none(),
|
||||
"branches must not inherit peer identity; the executor provisions per-branch assignments"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_for_child_resets_peer_fields() {
|
||||
let mut parent = create_test_ctx();
|
||||
parent.peer_registry = Some(Arc::new(PeerRegistry::new()));
|
||||
parent.peer_assignment = Some(("graph_agent_a_1".to_string(), Arc::new(Inbox::new())));
|
||||
|
||||
let child = RequestContext::new_for_child(
|
||||
Arc::clone(&parent.app),
|
||||
&parent,
|
||||
1,
|
||||
Arc::new(Inbox::new()),
|
||||
"agent_test_1".to_string(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
child.peer_registry.is_none() && child.peer_assignment.is_none(),
|
||||
"children must not inherit the parent frontier's peer roster"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escalation_queue_defaults_to_none() {
|
||||
let ctx = create_test_ctx();
|
||||
|
||||
Reference in new issue
Block a user