chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, mpsc, Mutex};
|
||||
use zclaw_types::{AgentConfig, AgentId, AgentInfo, Event, Result};
|
||||
use zclaw_types::{AgentConfig, AgentId, AgentInfo, Capability, Event, Result, HandRun, HandRunId, HandRunStatus, HandRunFilter, TriggerSource};
|
||||
#[cfg(feature = "multi-agent")]
|
||||
use zclaw_protocols::{A2aRouter, A2aAgentProfile, A2aCapability, A2aEnvelope, A2aMessageType, A2aRecipient};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -12,7 +14,7 @@ use crate::capabilities::CapabilityManager;
|
||||
use crate::events::EventBus;
|
||||
use crate::config::KernelConfig;
|
||||
use zclaw_memory::MemoryStore;
|
||||
use zclaw_runtime::{AgentLoop, LlmDriver, ToolRegistry, tool::SkillExecutor};
|
||||
use zclaw_runtime::{AgentLoop, LlmDriver, ToolRegistry, tool::SkillExecutor, tool::builtin::PathValidator};
|
||||
use zclaw_skills::SkillRegistry;
|
||||
use zclaw_skills::LlmCompleter;
|
||||
use zclaw_hands::{HandRegistry, HandContext, HandResult, hands::{BrowserHand, SlideshowHand, SpeechHand, QuizHand, WhiteboardHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, quiz::LlmQuizGenerator}};
|
||||
@@ -20,6 +22,8 @@ use zclaw_hands::{HandRegistry, HandContext, HandResult, hands::{BrowserHand, Sl
|
||||
/// Adapter that bridges `zclaw_runtime::LlmDriver` → `zclaw_skills::LlmCompleter`
|
||||
struct LlmDriverAdapter {
|
||||
driver: Arc<dyn LlmDriver>,
|
||||
max_tokens: u32,
|
||||
temperature: f32,
|
||||
}
|
||||
|
||||
impl zclaw_skills::LlmCompleter for LlmDriverAdapter {
|
||||
@@ -32,8 +36,8 @@ impl zclaw_skills::LlmCompleter for LlmDriverAdapter {
|
||||
Box::pin(async move {
|
||||
let request = zclaw_runtime::CompletionRequest {
|
||||
messages: vec![zclaw_types::Message::user(prompt)],
|
||||
max_tokens: Some(4096),
|
||||
temperature: Some(0.7),
|
||||
max_tokens: Some(self.max_tokens),
|
||||
temperature: Some(self.temperature),
|
||||
..Default::default()
|
||||
};
|
||||
let response = driver.complete(request).await
|
||||
@@ -59,7 +63,7 @@ pub struct KernelSkillExecutor {
|
||||
|
||||
impl KernelSkillExecutor {
|
||||
pub fn new(skills: Arc<SkillRegistry>, driver: Arc<dyn LlmDriver>) -> Self {
|
||||
let llm: Arc<dyn zclaw_skills::LlmCompleter> = Arc::new(LlmDriverAdapter { driver });
|
||||
let llm: Arc<dyn zclaw_skills::LlmCompleter> = Arc::new(LlmDriverAdapter { driver, max_tokens: 4096, temperature: 0.7 });
|
||||
Self { skills, llm }
|
||||
}
|
||||
}
|
||||
@@ -98,6 +102,14 @@ pub struct Kernel {
|
||||
hands: Arc<HandRegistry>,
|
||||
trigger_manager: crate::trigger_manager::TriggerManager,
|
||||
pending_approvals: Arc<Mutex<Vec<ApprovalEntry>>>,
|
||||
/// Running hand runs that can be cancelled (run_id -> cancelled flag)
|
||||
running_hand_runs: Arc<dashmap::DashMap<HandRunId, Arc<std::sync::atomic::AtomicBool>>>,
|
||||
/// A2A router for inter-agent messaging (gated by multi-agent feature)
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_router: Arc<A2aRouter>,
|
||||
/// Per-agent A2A inbox receivers
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_inboxes: Arc<dashmap::DashMap<AgentId, Arc<Mutex<mpsc::Receiver<A2aEnvelope>>>>>,
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
@@ -143,7 +155,11 @@ impl Kernel {
|
||||
|
||||
// Create LLM completer for skill system (shared with skill_executor)
|
||||
let llm_completer: Arc<dyn zclaw_skills::LlmCompleter> =
|
||||
Arc::new(LlmDriverAdapter { driver: driver.clone() });
|
||||
Arc::new(LlmDriverAdapter {
|
||||
driver: driver.clone(),
|
||||
max_tokens: config.max_tokens(),
|
||||
temperature: config.temperature(),
|
||||
});
|
||||
|
||||
// Initialize trigger manager
|
||||
let trigger_manager = crate::trigger_manager::TriggerManager::new(hands.clone());
|
||||
@@ -154,6 +170,13 @@ impl Kernel {
|
||||
registry.register(agent);
|
||||
}
|
||||
|
||||
// Initialize A2A router for multi-agent support
|
||||
#[cfg(feature = "multi-agent")]
|
||||
let a2a_router = {
|
||||
let kernel_agent_id = AgentId::new();
|
||||
Arc::new(A2aRouter::new(kernel_agent_id))
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
registry,
|
||||
@@ -167,6 +190,11 @@ impl Kernel {
|
||||
hands,
|
||||
trigger_manager,
|
||||
pending_approvals: Arc::new(Mutex::new(Vec::new())),
|
||||
running_hand_runs: Arc::new(dashmap::DashMap::new()),
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_router,
|
||||
#[cfg(feature = "multi-agent")]
|
||||
a2a_inboxes: Arc::new(dashmap::DashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,8 +322,17 @@ impl Kernel {
|
||||
self.memory.save_agent(&config).await?;
|
||||
|
||||
// Register in registry
|
||||
let config_clone = config.clone();
|
||||
self.registry.register(config);
|
||||
|
||||
// Register with A2A router for multi-agent messaging
|
||||
#[cfg(feature = "multi-agent")]
|
||||
{
|
||||
let profile = Self::agent_config_to_a2a_profile(&config_clone);
|
||||
let rx = self.a2a_router.register_agent(profile).await;
|
||||
self.a2a_inboxes.insert(id, Arc::new(Mutex::new(rx)));
|
||||
}
|
||||
|
||||
// Emit event
|
||||
self.events.publish(Event::AgentSpawned {
|
||||
agent_id: id,
|
||||
@@ -313,6 +350,13 @@ impl Kernel {
|
||||
// Remove from memory
|
||||
self.memory.delete_agent(id).await?;
|
||||
|
||||
// Unregister from A2A router
|
||||
#[cfg(feature = "multi-agent")]
|
||||
{
|
||||
self.a2a_router.unregister_agent(id).await;
|
||||
self.a2a_inboxes.remove(id);
|
||||
}
|
||||
|
||||
// Emit event
|
||||
self.events.publish(Event::AgentTerminated {
|
||||
agent_id: *id,
|
||||
@@ -346,7 +390,7 @@ impl Kernel {
|
||||
|
||||
// Create agent loop with model configuration
|
||||
let tools = self.create_tool_registry();
|
||||
let loop_runner = AgentLoop::new(
|
||||
let mut loop_runner = AgentLoop::new(
|
||||
*agent_id,
|
||||
self.driver.clone(),
|
||||
tools,
|
||||
@@ -356,7 +400,22 @@ impl Kernel {
|
||||
.with_skill_executor(self.skill_executor.clone())
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()))
|
||||
.with_compaction_threshold(15_000); // Compact when context exceeds ~15k tokens
|
||||
.with_compaction_threshold(
|
||||
agent_config.compaction_threshold
|
||||
.map(|t| t as usize)
|
||||
.unwrap_or_else(|| self.config.compaction_threshold()),
|
||||
);
|
||||
|
||||
// Set path validator from agent's workspace directory (if configured)
|
||||
if let Some(ref workspace) = agent_config.workspace {
|
||||
let path_validator = PathValidator::new().with_workspace(workspace.clone());
|
||||
tracing::info!(
|
||||
"[Kernel] Setting path_validator with workspace: {} for agent {}",
|
||||
workspace.display(),
|
||||
agent_id
|
||||
);
|
||||
loop_runner = loop_runner.with_path_validator(path_validator);
|
||||
}
|
||||
|
||||
// Build system prompt with skill information injected
|
||||
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref()).await;
|
||||
@@ -378,21 +437,35 @@ impl Kernel {
|
||||
agent_id: &AgentId,
|
||||
message: String,
|
||||
) -> Result<mpsc::Receiver<zclaw_runtime::LoopEvent>> {
|
||||
self.send_message_stream_with_prompt(agent_id, message, None).await
|
||||
self.send_message_stream_with_prompt(agent_id, message, None, None).await
|
||||
}
|
||||
|
||||
/// Send a message with streaming and optional external system prompt
|
||||
/// Send a message with streaming, optional system prompt, and optional session reuse
|
||||
pub async fn send_message_stream_with_prompt(
|
||||
&self,
|
||||
agent_id: &AgentId,
|
||||
message: String,
|
||||
system_prompt_override: Option<String>,
|
||||
session_id_override: Option<zclaw_types::SessionId>,
|
||||
) -> Result<mpsc::Receiver<zclaw_runtime::LoopEvent>> {
|
||||
let agent_config = self.registry.get(agent_id)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
||||
|
||||
// Create session
|
||||
let session_id = self.memory.create_session(agent_id).await?;
|
||||
// Reuse existing session or create new one
|
||||
let session_id = match session_id_override {
|
||||
Some(id) => {
|
||||
// Verify the session exists; if not, create a new one
|
||||
let existing = self.memory.get_messages(&id).await;
|
||||
match existing {
|
||||
Ok(msgs) if !msgs.is_empty() => id,
|
||||
_ => {
|
||||
tracing::debug!("Session {} not found or empty, creating new session", id);
|
||||
self.memory.create_session(agent_id).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
None => self.memory.create_session(agent_id).await?,
|
||||
};
|
||||
|
||||
// Always use Kernel's current model configuration
|
||||
// This ensures user's "模型与 API" settings are respected
|
||||
@@ -400,7 +473,7 @@ impl Kernel {
|
||||
|
||||
// Create agent loop with model configuration
|
||||
let tools = self.create_tool_registry();
|
||||
let loop_runner = AgentLoop::new(
|
||||
let mut loop_runner = AgentLoop::new(
|
||||
*agent_id,
|
||||
self.driver.clone(),
|
||||
tools,
|
||||
@@ -410,7 +483,23 @@ impl Kernel {
|
||||
.with_skill_executor(self.skill_executor.clone())
|
||||
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()))
|
||||
.with_compaction_threshold(15_000); // Compact when context exceeds ~15k tokens
|
||||
.with_compaction_threshold(
|
||||
agent_config.compaction_threshold
|
||||
.map(|t| t as usize)
|
||||
.unwrap_or_else(|| self.config.compaction_threshold()),
|
||||
);
|
||||
|
||||
// Set path validator from agent's workspace directory (if configured)
|
||||
// This enables file_read / file_write tools to access the workspace
|
||||
if let Some(ref workspace) = agent_config.workspace {
|
||||
let path_validator = PathValidator::new().with_workspace(workspace.clone());
|
||||
tracing::info!(
|
||||
"[Kernel] Setting path_validator with workspace: {} for agent {}",
|
||||
workspace.display(),
|
||||
agent_id
|
||||
);
|
||||
loop_runner = loop_runner.with_path_validator(path_validator);
|
||||
}
|
||||
|
||||
// Use external prompt if provided, otherwise build default
|
||||
let system_prompt = match system_prompt_override {
|
||||
@@ -489,15 +578,194 @@ impl Kernel {
|
||||
self.hands.list().await
|
||||
}
|
||||
|
||||
/// Execute a hand with the given input
|
||||
/// Execute a hand with the given input, tracking the run
|
||||
pub async fn execute_hand(
|
||||
&self,
|
||||
hand_id: &str,
|
||||
input: serde_json::Value,
|
||||
) -> Result<HandResult> {
|
||||
// Use default context (agent_id will be generated)
|
||||
) -> Result<(HandResult, HandRunId)> {
|
||||
let run_id = HandRunId::new();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Create the initial HandRun record
|
||||
let mut run = HandRun {
|
||||
id: run_id,
|
||||
hand_name: hand_id.to_string(),
|
||||
trigger_source: TriggerSource::Manual,
|
||||
params: input.clone(),
|
||||
status: HandRunStatus::Pending,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
created_at: now.clone(),
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
};
|
||||
self.memory.save_hand_run(&run).await?;
|
||||
|
||||
// Transition to Running
|
||||
run.status = HandRunStatus::Running;
|
||||
run.started_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
self.memory.update_hand_run(&run).await?;
|
||||
|
||||
// Register cancellation flag
|
||||
let cancel_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
self.running_hand_runs.insert(run_id, cancel_flag.clone());
|
||||
|
||||
// Execute the hand
|
||||
let context = HandContext::default();
|
||||
self.hands.execute(hand_id, &context, input).await
|
||||
let start = std::time::Instant::now();
|
||||
let hand_result = self.hands.execute(hand_id, &context, input).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Check if cancelled during execution
|
||||
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let mut run_update = run.clone();
|
||||
run_update.status = HandRunStatus::Cancelled;
|
||||
run_update.completed_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
run_update.duration_ms = Some(duration.as_millis() as u64);
|
||||
self.memory.update_hand_run(&run_update).await?;
|
||||
self.running_hand_runs.remove(&run_id);
|
||||
return Err(zclaw_types::ZclawError::Internal("Hand execution cancelled".to_string()));
|
||||
}
|
||||
|
||||
// Remove from running map
|
||||
self.running_hand_runs.remove(&run_id);
|
||||
|
||||
// Update HandRun with result
|
||||
let completed_at = chrono::Utc::now().to_rfc3339();
|
||||
match &hand_result {
|
||||
Ok(res) => {
|
||||
run.status = HandRunStatus::Completed;
|
||||
run.result = Some(res.output.clone());
|
||||
run.error = res.error.clone();
|
||||
}
|
||||
Err(e) => {
|
||||
run.status = HandRunStatus::Failed;
|
||||
run.error = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
run.duration_ms = Some(duration.as_millis() as u64);
|
||||
run.completed_at = Some(completed_at);
|
||||
self.memory.update_hand_run(&run).await?;
|
||||
|
||||
hand_result.map(|res| (res, run_id))
|
||||
}
|
||||
|
||||
/// Execute a hand with a specific trigger source (for scheduled/event triggers)
|
||||
pub async fn execute_hand_with_source(
|
||||
&self,
|
||||
hand_id: &str,
|
||||
input: serde_json::Value,
|
||||
trigger_source: TriggerSource,
|
||||
) -> Result<(HandResult, HandRunId)> {
|
||||
let run_id = HandRunId::new();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let mut run = HandRun {
|
||||
id: run_id,
|
||||
hand_name: hand_id.to_string(),
|
||||
trigger_source,
|
||||
params: input.clone(),
|
||||
status: HandRunStatus::Pending,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
created_at: now,
|
||||
started_at: None,
|
||||
completed_at: None,
|
||||
};
|
||||
self.memory.save_hand_run(&run).await?;
|
||||
|
||||
run.status = HandRunStatus::Running;
|
||||
run.started_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
self.memory.update_hand_run(&run).await?;
|
||||
|
||||
let cancel_flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
self.running_hand_runs.insert(run_id, cancel_flag.clone());
|
||||
|
||||
let context = HandContext::default();
|
||||
let start = std::time::Instant::now();
|
||||
let hand_result = self.hands.execute(hand_id, &context, input).await;
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Check if cancelled during execution
|
||||
if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
run.status = HandRunStatus::Cancelled;
|
||||
run.completed_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
run.duration_ms = Some(duration.as_millis() as u64);
|
||||
self.memory.update_hand_run(&run).await?;
|
||||
self.running_hand_runs.remove(&run_id);
|
||||
return Err(zclaw_types::ZclawError::Internal("Hand execution cancelled".to_string()));
|
||||
}
|
||||
|
||||
self.running_hand_runs.remove(&run_id);
|
||||
|
||||
let completed_at = chrono::Utc::now().to_rfc3339();
|
||||
match &hand_result {
|
||||
Ok(res) => {
|
||||
run.status = HandRunStatus::Completed;
|
||||
run.result = Some(res.output.clone());
|
||||
run.error = res.error.clone();
|
||||
}
|
||||
Err(e) => {
|
||||
run.status = HandRunStatus::Failed;
|
||||
run.error = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
run.duration_ms = Some(duration.as_millis() as u64);
|
||||
run.completed_at = Some(completed_at);
|
||||
self.memory.update_hand_run(&run).await?;
|
||||
|
||||
hand_result.map(|res| (res, run_id))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Hand Run Tracking
|
||||
// ============================================================
|
||||
|
||||
/// Get a hand run by ID
|
||||
pub async fn get_hand_run(&self, id: &HandRunId) -> Result<Option<HandRun>> {
|
||||
self.memory.get_hand_run(id).await
|
||||
}
|
||||
|
||||
/// List hand runs with filter
|
||||
pub async fn list_hand_runs(&self, filter: &HandRunFilter) -> Result<Vec<HandRun>> {
|
||||
self.memory.list_hand_runs(filter).await
|
||||
}
|
||||
|
||||
/// Count hand runs matching filter
|
||||
pub async fn count_hand_runs(&self, filter: &HandRunFilter) -> Result<u32> {
|
||||
self.memory.count_hand_runs(filter).await
|
||||
}
|
||||
|
||||
/// Cancel a running hand execution
|
||||
pub async fn cancel_hand_run(&self, id: &HandRunId) -> Result<()> {
|
||||
if let Some((_, flag)) = self.running_hand_runs.remove(id) {
|
||||
flag.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Note: the actual status update happens in execute_hand_with_source
|
||||
// when it detects the cancel flag
|
||||
Ok(())
|
||||
} else {
|
||||
// Not currently running — check if exists at all
|
||||
let run = self.memory.get_hand_run(id).await?;
|
||||
match run {
|
||||
Some(r) if r.status == HandRunStatus::Pending => {
|
||||
let mut updated = r;
|
||||
updated.status = HandRunStatus::Cancelled;
|
||||
updated.completed_at = Some(chrono::Utc::now().to_rfc3339());
|
||||
self.memory.update_hand_run(&updated).await?;
|
||||
Ok(())
|
||||
}
|
||||
Some(r) => Err(zclaw_types::ZclawError::InvalidInput(
|
||||
format!("Cannot cancel hand run {} with status {}", id, r.status)
|
||||
)),
|
||||
None => Err(zclaw_types::ZclawError::NotFound(
|
||||
format!("Hand run {} not found", id)
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -563,6 +831,7 @@ impl Kernel {
|
||||
status: "pending".to_string(),
|
||||
created_at: chrono::Utc::now(),
|
||||
input,
|
||||
reject_reason: None,
|
||||
};
|
||||
let mut approvals = self.pending_approvals.lock().await;
|
||||
approvals.push(entry.clone());
|
||||
@@ -574,13 +843,16 @@ impl Kernel {
|
||||
&self,
|
||||
id: &str,
|
||||
approved: bool,
|
||||
_reason: Option<String>,
|
||||
reason: Option<String>,
|
||||
) -> Result<()> {
|
||||
let mut approvals = self.pending_approvals.lock().await;
|
||||
let entry = approvals.iter_mut().find(|a| a.id == id && a.status == "pending")
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Approval not found: {}", id)))?;
|
||||
|
||||
entry.status = if approved { "approved".to_string() } else { "rejected".to_string() };
|
||||
if let Some(r) = reason {
|
||||
entry.reject_reason = Some(r);
|
||||
}
|
||||
|
||||
if approved {
|
||||
let hand_id = entry.hand_id.clone();
|
||||
@@ -623,9 +895,268 @@ impl Kernel {
|
||||
entry.status = "cancelled".to_string();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Approval entry for pending approvals
|
||||
// ============================================================
|
||||
// A2A (Agent-to-Agent) Messaging
|
||||
// ============================================================
|
||||
|
||||
/// Derive an A2A agent profile from an AgentConfig
|
||||
#[cfg(feature = "multi-agent")]
|
||||
fn agent_config_to_a2a_profile(config: &AgentConfig) -> A2aAgentProfile {
|
||||
let caps: Vec<A2aCapability> = config.tools.iter().map(|tool_name| {
|
||||
A2aCapability {
|
||||
name: tool_name.clone(),
|
||||
description: format!("Tool: {}", tool_name),
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
requires_approval: false,
|
||||
version: "1.0.0".to_string(),
|
||||
tags: vec![],
|
||||
}
|
||||
}).collect();
|
||||
|
||||
A2aAgentProfile {
|
||||
id: config.id,
|
||||
name: config.name.clone(),
|
||||
description: config.description.clone().unwrap_or_default(),
|
||||
capabilities: caps,
|
||||
protocols: vec!["a2a".to_string()],
|
||||
role: "worker".to_string(),
|
||||
priority: 5,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
groups: vec![],
|
||||
last_seen: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an agent is authorized to send messages to a target
|
||||
#[cfg(feature = "multi-agent")]
|
||||
fn check_a2a_permission(&self, from: &AgentId, to: &AgentId) -> Result<()> {
|
||||
let caps = self.capabilities.get(from);
|
||||
match caps {
|
||||
Some(cap_set) => {
|
||||
let has_permission = cap_set.capabilities.iter().any(|cap| {
|
||||
match cap {
|
||||
Capability::AgentMessage { pattern } => {
|
||||
pattern == "*" || to.to_string().starts_with(pattern)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if !has_permission {
|
||||
return Err(zclaw_types::ZclawError::PermissionDenied(
|
||||
format!("Agent {} does not have AgentMessage capability for {}", from, to)
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
// No capabilities registered — deny by default
|
||||
Err(zclaw_types::ZclawError::PermissionDenied(
|
||||
format!("Agent {} has no capabilities registered", from)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a direct A2A message from one agent to another
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_send(
|
||||
&self,
|
||||
from: &AgentId,
|
||||
to: &AgentId,
|
||||
payload: serde_json::Value,
|
||||
message_type: Option<A2aMessageType>,
|
||||
) -> Result<()> {
|
||||
// Validate sender exists
|
||||
self.registry.get(from)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Sender agent not found: {}", from)
|
||||
))?;
|
||||
|
||||
// Validate receiver exists and is running
|
||||
self.registry.get(to)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Target agent not found: {}", to)
|
||||
))?;
|
||||
|
||||
// Check capability permission
|
||||
self.check_a2a_permission(from, to)?;
|
||||
|
||||
// Build and route envelope
|
||||
let envelope = A2aEnvelope::new(
|
||||
*from,
|
||||
A2aRecipient::Direct { agent_id: *to },
|
||||
message_type.unwrap_or(A2aMessageType::Notification),
|
||||
payload,
|
||||
);
|
||||
|
||||
self.a2a_router.route(envelope).await?;
|
||||
|
||||
// Emit event
|
||||
self.events.publish(Event::A2aMessageSent {
|
||||
from: *from,
|
||||
to: format!("{}", to),
|
||||
message_type: "direct".to_string(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Broadcast a message from one agent to all other agents
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_broadcast(
|
||||
&self,
|
||||
from: &AgentId,
|
||||
payload: serde_json::Value,
|
||||
) -> Result<()> {
|
||||
// Validate sender exists
|
||||
self.registry.get(from)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Sender agent not found: {}", from)
|
||||
))?;
|
||||
|
||||
let envelope = A2aEnvelope::new(
|
||||
*from,
|
||||
A2aRecipient::Broadcast,
|
||||
A2aMessageType::Notification,
|
||||
payload,
|
||||
);
|
||||
|
||||
self.a2a_router.route(envelope).await?;
|
||||
|
||||
self.events.publish(Event::A2aMessageSent {
|
||||
from: *from,
|
||||
to: "broadcast".to_string(),
|
||||
message_type: "broadcast".to_string(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover agents that have a specific capability
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_discover(&self, capability: &str) -> Result<Vec<A2aAgentProfile>> {
|
||||
let result = self.a2a_router.discover(capability).await?;
|
||||
|
||||
self.events.publish(Event::A2aAgentDiscovered {
|
||||
agent_id: AgentId::new(),
|
||||
capabilities: vec![capability.to_string()],
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Try to receive a pending A2A message for an agent (non-blocking)
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_receive(&self, agent_id: &AgentId) -> Result<Option<A2aEnvelope>> {
|
||||
let inbox = self.a2a_inboxes.get(agent_id)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("No A2A inbox for agent: {}", agent_id)
|
||||
))?;
|
||||
|
||||
let mut rx = inbox.lock().await;
|
||||
match rx.try_recv() {
|
||||
Ok(envelope) => {
|
||||
self.events.publish(Event::A2aMessageReceived {
|
||||
from: envelope.from,
|
||||
to: format!("{}", agent_id),
|
||||
message_type: "direct".to_string(),
|
||||
});
|
||||
Ok(Some(envelope))
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate a task to another agent and wait for response with timeout
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_delegate_task(
|
||||
&self,
|
||||
from: &AgentId,
|
||||
to: &AgentId,
|
||||
task_description: String,
|
||||
timeout_ms: u64,
|
||||
) -> Result<serde_json::Value> {
|
||||
// Validate both agents exist
|
||||
self.registry.get(from)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Sender agent not found: {}", from)
|
||||
))?;
|
||||
self.registry.get(to)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Target agent not found: {}", to)
|
||||
))?;
|
||||
|
||||
// Check capability permission
|
||||
self.check_a2a_permission(from, to)?;
|
||||
|
||||
// Send task request
|
||||
let task_id = uuid::Uuid::new_v4().to_string();
|
||||
let envelope = A2aEnvelope::new(
|
||||
*from,
|
||||
A2aRecipient::Direct { agent_id: *to },
|
||||
A2aMessageType::Task,
|
||||
serde_json::json!({
|
||||
"task_id": task_id,
|
||||
"description": task_description,
|
||||
}),
|
||||
).with_conversation(task_id.clone());
|
||||
|
||||
let envelope_id = envelope.id.clone();
|
||||
self.a2a_router.route(envelope).await?;
|
||||
|
||||
self.events.publish(Event::A2aMessageSent {
|
||||
from: *from,
|
||||
to: format!("{}", to),
|
||||
message_type: "task".to_string(),
|
||||
});
|
||||
|
||||
// Wait for response with timeout
|
||||
let timeout = tokio::time::Duration::from_millis(timeout_ms);
|
||||
let result = tokio::time::timeout(timeout, async {
|
||||
let inbox = self.a2a_inboxes.get(from)
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("No A2A inbox for agent: {}", from)
|
||||
))?;
|
||||
let mut rx = inbox.lock().await;
|
||||
|
||||
// Poll for matching response
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Some(msg) => {
|
||||
// Check if this is a response to our task
|
||||
if msg.message_type == A2aMessageType::Response
|
||||
&& msg.reply_to.as_deref() == Some(&envelope_id) {
|
||||
return Ok::<_, zclaw_types::ZclawError>(msg.payload);
|
||||
}
|
||||
// Not our response — put it back by logging it (would need a re-queue mechanism for production)
|
||||
tracing::warn!("Received non-matching A2A response, discarding: {}", msg.id);
|
||||
}
|
||||
None => {
|
||||
return Err(zclaw_types::ZclawError::Internal(
|
||||
"A2A inbox channel closed".to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(payload)) => Ok(payload),
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(_) => Err(zclaw_types::ZclawError::Timeout(
|
||||
format!("A2A task delegation timed out after {}ms", timeout_ms)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all online agents via A2A profiles
|
||||
#[cfg(feature = "multi-agent")]
|
||||
pub async fn a2a_get_online_agents(&self) -> Result<Vec<A2aAgentProfile>> {
|
||||
Ok(self.a2a_router.list_profiles().await)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApprovalEntry {
|
||||
pub id: String,
|
||||
@@ -633,6 +1164,7 @@ pub struct ApprovalEntry {
|
||||
pub status: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub input: serde_json::Value,
|
||||
pub reject_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from sending a message
|
||||
|
||||
Reference in New Issue
Block a user