From 9af7b0dd46e6be2730d1647e61121c99ce216e2c Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 4 Apr 2026 09:41:24 +0800 Subject: [PATCH] fix(kernel): enable multi-agent compilation + A2A routing tests - director.rs: add missing CompletionRequest fields (thinking_enabled, reasoning_effort, plan_mode) for multi-agent feature gate - agents.rs: remove unused AgentState import behind multi-agent feature - lib.rs: replace ambiguous glob re-export with explicit director types, resolving AgentRole conflict between director and generation modules - a2a.rs: add 5 integration tests covering direct message delivery, broadcast routing, group messaging, agent unregistration, and expired message rejection (10 total A2A tests, all passing) - Verified: 537 workspace tests pass with multi-agent feature enabled Co-Authored-By: Claude Opus 4.6 --- crates/zclaw-kernel/src/director.rs | 3 + crates/zclaw-kernel/src/kernel/agents.rs | 2 +- crates/zclaw-kernel/src/lib.rs | 6 +- crates/zclaw-protocols/src/a2a.rs | 180 +++++++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/crates/zclaw-kernel/src/director.rs b/crates/zclaw-kernel/src/director.rs index 83f4981..c032767 100644 --- a/crates/zclaw-kernel/src/director.rs +++ b/crates/zclaw-kernel/src/director.rs @@ -448,6 +448,9 @@ Respond with ONLY the number (1-{}) of the agent who should speak next. No expla temperature: Some(0.3), stop: vec![], stream: false, + thinking_enabled: false, + reasoning_effort: None, + plan_mode: false, }; match driver.complete(request).await { diff --git a/crates/zclaw-kernel/src/kernel/agents.rs b/crates/zclaw-kernel/src/kernel/agents.rs index 07625da..7fcb859 100644 --- a/crates/zclaw-kernel/src/kernel/agents.rs +++ b/crates/zclaw-kernel/src/kernel/agents.rs @@ -1,6 +1,6 @@ //! Agent CRUD operations -use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState, Event, Result}; +use zclaw_types::{AgentConfig, AgentId, AgentInfo, Event, Result}; #[cfg(feature = "multi-agent")] use std::sync::Arc; diff --git a/crates/zclaw-kernel/src/lib.rs b/crates/zclaw-kernel/src/lib.rs index 34795d9..b5c69d3 100644 --- a/crates/zclaw-kernel/src/lib.rs +++ b/crates/zclaw-kernel/src/lib.rs @@ -22,7 +22,11 @@ pub use events::*; pub use config::*; pub use trigger_manager::{TriggerManager, TriggerEntry, TriggerUpdateRequest, TriggerManagerConfig}; #[cfg(feature = "multi-agent")] -pub use director::*; +pub use director::{ + Director, DirectorConfig, DirectorBuilder, DirectorAgent, + ConversationState, ScheduleStrategy, + // Note: AgentRole is intentionally NOT re-exported here — use generation::AgentRole instead +}; #[cfg(feature = "multi-agent")] pub use zclaw_protocols::{ A2aRouter, A2aAgentProfile, A2aCapability, A2aEnvelope, A2aMessageType, A2aRecipient, diff --git a/crates/zclaw-protocols/src/a2a.rs b/crates/zclaw-protocols/src/a2a.rs index 9e74404..c967e6d 100644 --- a/crates/zclaw-protocols/src/a2a.rs +++ b/crates/zclaw-protocols/src/a2a.rs @@ -754,4 +754,184 @@ mod tests { let cap_index = router.capability_index.read().await; assert!(cap_index.contains_key("code-generation")); } + + #[tokio::test] + async fn test_direct_message_delivery() { + let router = A2aRouter::new(AgentId::new()); + + // Register two agents + let alice_id = AgentId::new(); + let bob_id = AgentId::new(); + + let alice_profile = A2aAgentProfile { + id: alice_id, name: "Alice".into(), description: String::new(), + capabilities: vec![], protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + let bob_profile = A2aAgentProfile { + id: bob_id, name: "Bob".into(), description: String::new(), + capabilities: vec![], protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + + let mut alice_rx = router.register_agent(alice_profile).await; + let mut bob_rx = router.register_agent(bob_profile).await; + + // Alice sends direct message to Bob + let envelope = A2aEnvelope::new( + alice_id, + A2aRecipient::Direct { agent_id: bob_id }, + A2aMessageType::Notification, + serde_json::json!({"msg": "hello bob"}), + ); + router.route(envelope).await.unwrap(); + + // Bob should receive it + let received = bob_rx.recv().await.unwrap(); + assert_eq!(received.from, alice_id); + assert_eq!(received.payload["msg"], "hello bob"); + + // Alice should NOT receive it + assert!(alice_rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_broadcast_delivery() { + let router = A2aRouter::new(AgentId::new()); + + let alice_id = AgentId::new(); + let bob_id = AgentId::new(); + let carol_id = AgentId::new(); + + let make_profile = |id: AgentId, name: &str| A2aAgentProfile { + id, name: name.into(), description: String::new(), + capabilities: vec![], protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + + let mut alice_rx = router.register_agent(make_profile(alice_id, "Alice")).await; + let mut bob_rx = router.register_agent(make_profile(bob_id, "Bob")).await; + let mut carol_rx = router.register_agent(make_profile(carol_id, "Carol")).await; + + // Alice broadcasts + let envelope = A2aEnvelope::new( + alice_id, + A2aRecipient::Broadcast, + A2aMessageType::Notification, + serde_json::json!({"announcement": "standup in 5"}), + ); + router.route(envelope).await.unwrap(); + + // Bob and Carol should receive, Alice should NOT (sender excluded) + let bob_msg = bob_rx.recv().await.unwrap(); + assert_eq!(bob_msg.payload["announcement"], "standup in 5"); + + let carol_msg = carol_rx.recv().await.unwrap(); + assert_eq!(carol_msg.payload["announcement"], "standup in 5"); + + assert!(alice_rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_group_message_delivery() { + let router = A2aRouter::new(AgentId::new()); + + let alice_id = AgentId::new(); + let bob_id = AgentId::new(); + let carol_id = AgentId::new(); + + let make_profile = |id: AgentId, name: &str| A2aAgentProfile { + id, name: name.into(), description: String::new(), + capabilities: vec![], protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + + let mut alice_rx = router.register_agent(make_profile(alice_id, "Alice")).await; + let mut bob_rx = router.register_agent(make_profile(bob_id, "Bob")).await; + let mut carol_rx = router.register_agent(make_profile(carol_id, "Carol")).await; + + // Add Bob and Carol to "dev-team" group + router.add_to_group("dev-team", bob_id).await; + router.add_to_group("dev-team", carol_id).await; + + // Alice sends to group + let envelope = A2aEnvelope::new( + alice_id, + A2aRecipient::Group { group_id: "dev-team".into() }, + A2aMessageType::Notification, + serde_json::json!({"sprint": "review"}), + ); + router.route(envelope).await.unwrap(); + + // Bob and Carol should receive + let bob_msg = bob_rx.recv().await.unwrap(); + assert_eq!(bob_msg.payload["sprint"], "review"); + let carol_msg = carol_rx.recv().await.unwrap(); + assert_eq!(carol_msg.payload["sprint"], "review"); + + // Alice not in group, should NOT receive + assert!(alice_rx.try_recv().is_err()); + } + + #[tokio::test] + async fn test_unregister_agent() { + let router = A2aRouter::new(AgentId::new()); + + let agent_id = AgentId::new(); + let profile = A2aAgentProfile { + id: agent_id, name: "Temp".into(), description: String::new(), + capabilities: vec![A2aCapability { + name: "temp-work".into(), description: "temp".into(), + input_schema: None, output_schema: None, requires_approval: false, + version: "1.0.0".into(), tags: vec![], + }], + protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + + router.register_agent(profile).await; + assert_eq!(router.list_profiles().await.len(), 1); + + // Discover should find it + let found = router.discover("temp-work").await.unwrap(); + assert_eq!(found.len(), 1); + + // Unregister + router.unregister_agent(&agent_id).await; + assert_eq!(router.list_profiles().await.len(), 0); + + // Capability index should be cleaned + let found_after = router.discover("temp-work").await.unwrap(); + assert!(found_after.is_empty()); + } + + #[tokio::test] + async fn test_expired_message_rejected() { + let router = A2aRouter::new(AgentId::new()); + + let alice_id = AgentId::new(); + let bob_id = AgentId::new(); + + let profile = |id: AgentId| A2aAgentProfile { + id, name: "Agent".into(), description: String::new(), + capabilities: vec![], protocols: vec!["a2a".into()], role: "worker".into(), + priority: 5, metadata: HashMap::new(), groups: vec![], last_seen: 0, + }; + + let _rx = router.register_agent(profile(alice_id)).await; + let _rx = router.register_agent(profile(bob_id)).await; + + // Create envelope with already-expired TTL + let mut envelope = A2aEnvelope::new( + alice_id, + A2aRecipient::Direct { agent_id: bob_id }, + A2aMessageType::Notification, + serde_json::json!({}), + ); + envelope.ttl = 1; // 1 second + envelope.timestamp = 0; // Far in the past — definitely expired + + let result = router.route(envelope).await; + assert!(result.is_err()); + } }