feat(multi-agent): enable Director + butler delegation (Chunk 4)
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Enable multi-agent feature by default in desktop build - Add butler delegation logic: task decomposition, expert assignment - Add ExpertTask, DelegationResult, butler_delegate() to Director - Add butler_delegate_task Tauri command bridging Director to frontend - 13 Director tests passing (6 original + 7 new butler tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -793,6 +793,246 @@ impl Default for DirectorBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Butler delegation — task decomposition and expert assignment
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A task assigned to an expert agent by the butler.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ExpertTask {
|
||||||
|
/// Unique task ID
|
||||||
|
pub id: String,
|
||||||
|
/// The sub-task description
|
||||||
|
pub description: String,
|
||||||
|
/// Assigned expert agent (if any)
|
||||||
|
pub assigned_expert: Option<DirectorAgent>,
|
||||||
|
/// Task category (logistics, compliance, customer, pricing, technology, general)
|
||||||
|
pub category: String,
|
||||||
|
/// Task priority (higher = more urgent)
|
||||||
|
pub priority: u8,
|
||||||
|
/// Current status
|
||||||
|
pub status: ExpertTaskStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status of an expert task.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ExpertTaskStatus {
|
||||||
|
#[default]
|
||||||
|
Pending,
|
||||||
|
Assigned,
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of butler delegation.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DelegationResult {
|
||||||
|
/// Original user request
|
||||||
|
pub request: String,
|
||||||
|
/// Decomposed sub-tasks with expert assignments
|
||||||
|
pub tasks: Vec<ExpertTask>,
|
||||||
|
/// Whether delegation was successful
|
||||||
|
pub success: bool,
|
||||||
|
/// Summary message for the user
|
||||||
|
pub summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Director {
|
||||||
|
/// Butler receives a user request, decomposes it into sub-tasks,
|
||||||
|
/// and assigns each to the best-matching registered expert agent.
|
||||||
|
///
|
||||||
|
/// If no LLM driver is available, falls back to rule-based decomposition.
|
||||||
|
pub async fn butler_delegate(&self, user_request: &str) -> Result<DelegationResult> {
|
||||||
|
let agents = self.get_active_agents().await;
|
||||||
|
|
||||||
|
// Decompose the request into sub-tasks
|
||||||
|
let subtasks = if self.llm_driver.is_some() {
|
||||||
|
self.decompose_with_llm(user_request).await?
|
||||||
|
} else {
|
||||||
|
Self::decompose_rule_based(user_request)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign experts to each sub-task
|
||||||
|
let tasks = self.assign_experts(&subtasks, &agents).await;
|
||||||
|
|
||||||
|
let summary = format!(
|
||||||
|
"已将您的需求拆解为 {} 个子任务{}。",
|
||||||
|
tasks.len(),
|
||||||
|
if tasks.iter().any(|t| t.assigned_expert.is_some()) {
|
||||||
|
",已分派给对应专家"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(DelegationResult {
|
||||||
|
request: user_request.to_string(),
|
||||||
|
tasks,
|
||||||
|
success: true,
|
||||||
|
summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use LLM to decompose a user request into structured sub-tasks.
|
||||||
|
async fn decompose_with_llm(&self, request: &str) -> Result<Vec<ExpertTask>> {
|
||||||
|
let driver = self.llm_driver.as_ref()
|
||||||
|
.ok_or_else(|| ZclawError::InvalidInput("No LLM driver configured".into()))?;
|
||||||
|
|
||||||
|
let prompt = format!(
|
||||||
|
r#"你是 ZCLAW 管家。请将以下用户需求拆解为 1-5 个具体子任务。
|
||||||
|
|
||||||
|
用户需求:{}
|
||||||
|
|
||||||
|
请按 JSON 数组格式输出,每个元素包含:
|
||||||
|
- description: 子任务描述(中文)
|
||||||
|
- category: 分类(logistics/compliance/customer/pricing/technology/general)
|
||||||
|
- priority: 优先级 1-10
|
||||||
|
|
||||||
|
只输出 JSON 数组,不要其他内容。"#,
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
let completion_request = CompletionRequest {
|
||||||
|
model: "default".to_string(),
|
||||||
|
system: Some("你是任务拆解专家,只输出 JSON。".to_string()),
|
||||||
|
messages: vec![zclaw_types::Message::User { content: prompt }],
|
||||||
|
tools: vec![],
|
||||||
|
max_tokens: Some(500),
|
||||||
|
temperature: Some(0.3),
|
||||||
|
stop: vec![],
|
||||||
|
stream: false,
|
||||||
|
thinking_enabled: false,
|
||||||
|
reasoning_effort: None,
|
||||||
|
plan_mode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
match driver.complete(completion_request).await {
|
||||||
|
Ok(response) => {
|
||||||
|
let text: String = response.content.iter()
|
||||||
|
.filter_map(|block| match block {
|
||||||
|
zclaw_runtime::ContentBlock::Text { text } => Some(text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Try to extract JSON array from response
|
||||||
|
let json_text = extract_json_array(&text);
|
||||||
|
match serde_json::from_str::<Vec<serde_json::Value>>(&json_text) {
|
||||||
|
Ok(items) => {
|
||||||
|
let tasks: Vec<ExpertTask> = items.into_iter().map(|item| {
|
||||||
|
ExpertTask {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
description: item.get("description")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("未命名任务")
|
||||||
|
.to_string(),
|
||||||
|
assigned_expert: None,
|
||||||
|
category: item.get("category")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("general")
|
||||||
|
.to_string(),
|
||||||
|
priority: item.get("priority")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(5) as u8,
|
||||||
|
status: ExpertTaskStatus::Pending,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
Ok(tasks)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Fallback: treat the whole request as one task
|
||||||
|
Ok(vec![ExpertTask {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
description: request.to_string(),
|
||||||
|
assigned_expert: None,
|
||||||
|
category: "general".to_string(),
|
||||||
|
priority: 5,
|
||||||
|
status: ExpertTaskStatus::Pending,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("LLM decomposition failed: {}, falling back to rule-based", e);
|
||||||
|
Ok(Self::decompose_rule_based(request))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rule-based decomposition for when no LLM is available.
|
||||||
|
fn decompose_rule_based(request: &str) -> Vec<ExpertTask> {
|
||||||
|
let category = classify_delegation_category(request);
|
||||||
|
vec![ExpertTask {
|
||||||
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
|
description: request.to_string(),
|
||||||
|
assigned_expert: None,
|
||||||
|
category,
|
||||||
|
priority: 5,
|
||||||
|
status: ExpertTaskStatus::Pending,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assign each task to the best-matching expert agent.
|
||||||
|
async fn assign_experts(
|
||||||
|
&self,
|
||||||
|
tasks: &[ExpertTask],
|
||||||
|
agents: &[DirectorAgent],
|
||||||
|
) -> Vec<ExpertTask> {
|
||||||
|
tasks.iter().map(|task| {
|
||||||
|
let best_match = agents.iter().find(|agent| {
|
||||||
|
agent.role == AgentRole::Expert
|
||||||
|
&& agent.persona.to_lowercase().contains(&task.category.to_lowercase())
|
||||||
|
}).or_else(|| {
|
||||||
|
// Fallback: find any expert
|
||||||
|
agents.iter().find(|agent| agent.role == AgentRole::Expert)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut assigned = task.clone();
|
||||||
|
if let Some(expert) = best_match {
|
||||||
|
assigned.assigned_expert = Some(expert.clone());
|
||||||
|
assigned.status = ExpertTaskStatus::Assigned;
|
||||||
|
}
|
||||||
|
assigned
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Classify a request into a delegation category based on keyword matching.
|
||||||
|
fn classify_delegation_category(text: &str) -> String {
|
||||||
|
let lower = text.to_lowercase();
|
||||||
|
// Check compliance first — "合规/法规/标准" are more specific than logistics keywords
|
||||||
|
if ["合规", "法规", "标准", "认证", "报检"].iter().any(|k| lower.contains(k)) {
|
||||||
|
"compliance".to_string()
|
||||||
|
} else if ["物流", "发货", "出口", "包", "运输", "仓库"].iter().any(|k| lower.contains(k)) {
|
||||||
|
"logistics".to_string()
|
||||||
|
} else if ["客户", "投诉", "反馈", "服务", "售后"].iter().any(|k| lower.contains(k)) {
|
||||||
|
"customer".to_string()
|
||||||
|
} else if ["报价", "价格", "成本", "利润", "预算"].iter().any(|k| lower.contains(k)) {
|
||||||
|
"pricing".to_string()
|
||||||
|
} else if ["系统", "软件", "电脑", "网络", "数据"].iter().any(|k| lower.contains(k)) {
|
||||||
|
"technology".to_string()
|
||||||
|
} else {
|
||||||
|
"general".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a JSON array from text that may contain surrounding prose.
|
||||||
|
fn extract_json_array(text: &str) -> String {
|
||||||
|
// Try to find content between [ and ]
|
||||||
|
if let Some(start) = text.find('[') {
|
||||||
|
if let Some(end) = text.rfind(']') {
|
||||||
|
if end > start {
|
||||||
|
return text[start..=end].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return original if no array brackets found
|
||||||
|
text.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -912,4 +1152,88 @@ mod tests {
|
|||||||
assert_eq!(AgentRole::from_str("STUDENT"), Some(AgentRole::Student));
|
assert_eq!(AgentRole::from_str("STUDENT"), Some(AgentRole::Student));
|
||||||
assert_eq!(AgentRole::from_str("unknown"), None);
|
assert_eq!(AgentRole::from_str("unknown"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Butler delegation tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_classify_delegation_category() {
|
||||||
|
assert_eq!(classify_delegation_category("这批物流要发往欧洲"), "logistics");
|
||||||
|
assert_eq!(classify_delegation_category("出口合规标准变了"), "compliance");
|
||||||
|
assert_eq!(classify_delegation_category("客户投诉太多了"), "customer");
|
||||||
|
assert_eq!(classify_delegation_category("报价需要调整"), "pricing");
|
||||||
|
assert_eq!(classify_delegation_category("系统又崩了"), "technology");
|
||||||
|
assert_eq!(classify_delegation_category("随便聊聊"), "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_json_array() {
|
||||||
|
let with_prose = "好的,分析如下:\n[{\"description\":\"分析物流\",\"category\":\"logistics\",\"priority\":8}]\n以上。";
|
||||||
|
let result = extract_json_array(with_prose);
|
||||||
|
assert!(result.starts_with('['));
|
||||||
|
assert!(result.ends_with(']'));
|
||||||
|
|
||||||
|
let bare = "[{\"a\":1}]";
|
||||||
|
assert_eq!(extract_json_array(bare), bare);
|
||||||
|
|
||||||
|
let no_array = "just text";
|
||||||
|
assert_eq!(extract_json_array(no_array), "just text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rule_based_decomposition() {
|
||||||
|
let tasks = Director::decompose_rule_based("出口包装需要整改");
|
||||||
|
assert_eq!(tasks.len(), 1);
|
||||||
|
// "包" matches logistics first
|
||||||
|
assert_eq!(tasks[0].category, "logistics");
|
||||||
|
assert_eq!(tasks[0].status, ExpertTaskStatus::Pending);
|
||||||
|
assert!(!tasks[0].id.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_butler_delegate_rule_based() {
|
||||||
|
let director = Director::new(DirectorConfig::default());
|
||||||
|
|
||||||
|
// Register an expert
|
||||||
|
director.register_agent(DirectorAgent::new(
|
||||||
|
AgentId::new(),
|
||||||
|
"合规专家",
|
||||||
|
AgentRole::Expert,
|
||||||
|
"擅长 compliance 和 logistics 领域",
|
||||||
|
)).await;
|
||||||
|
|
||||||
|
let result = director.butler_delegate("出口包装被退回了,需要整改").await.unwrap();
|
||||||
|
assert!(result.success);
|
||||||
|
assert!(result.summary.contains("拆解为"));
|
||||||
|
assert_eq!(result.tasks.len(), 1);
|
||||||
|
// Expert should be assigned (matches category)
|
||||||
|
assert!(result.tasks[0].assigned_expert.is_some());
|
||||||
|
assert_eq!(result.tasks[0].status, ExpertTaskStatus::Assigned);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_butler_delegate_no_experts() {
|
||||||
|
let director = Director::new(DirectorConfig::default());
|
||||||
|
// No agents registered
|
||||||
|
let result = director.butler_delegate("帮我查一下物流状态").await.unwrap();
|
||||||
|
assert!(result.success);
|
||||||
|
assert!(result.tasks[0].assigned_expert.is_none());
|
||||||
|
assert_eq!(result.tasks[0].status, ExpertTaskStatus::Pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expert_task_serialization() {
|
||||||
|
let task = ExpertTask {
|
||||||
|
id: "test-id".to_string(),
|
||||||
|
description: "测试任务".to_string(),
|
||||||
|
assigned_expert: None,
|
||||||
|
category: "logistics".to_string(),
|
||||||
|
priority: 8,
|
||||||
|
status: ExpertTaskStatus::Assigned,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_string(&task).unwrap();
|
||||||
|
let decoded: ExpertTask = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(decoded.id, "test-id");
|
||||||
|
assert_eq!(decoded.category, "logistics");
|
||||||
|
assert_eq!(decoded.status, ExpertTaskStatus::Assigned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["multi-agent"]
|
||||||
# Multi-agent orchestration (A2A protocol, Director, agent delegation)
|
# Multi-agent orchestration (A2A protocol, Director, agent delegation)
|
||||||
# Disabled by default — enable when multi-agent UI is ready.
|
|
||||||
multi-agent = ["zclaw-kernel/multi-agent"]
|
multi-agent = ["zclaw-kernel/multi-agent"]
|
||||||
dev-server = ["dep:axum", "dep:tower-http"]
|
dev-server = ["dep:axum", "dep:tower-http"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! A2A (Agent-to-Agent) commands — gated behind `multi-agent` feature
|
//! A2A (Agent-to-Agent) commands — gated behind `multi-agent` feature
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use zclaw_types::AgentId;
|
use zclaw_types::AgentId;
|
||||||
@@ -109,10 +110,89 @@ pub async fn agent_a2a_delegate_task(
|
|||||||
|
|
||||||
let timeout = timeout_ms.unwrap_or(30_000);
|
let timeout = timeout_ms.unwrap_or(30_000);
|
||||||
|
|
||||||
// 30 seconds default
|
|
||||||
|
|
||||||
let response = kernel.a2a_delegate_task(&from_id, &to_id, task, timeout).await
|
let response = kernel.a2a_delegate_task(&from_id, &to_id, task, timeout).await
|
||||||
.map_err(|e| format!("A2A task delegation failed: {}", e))?;
|
.map_err(|e| format!("A2A task delegation failed: {}", e))?;
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Butler Delegation Command — multi-agent feature
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// Result of butler task delegation (mirrors zclaw_kernel::director::DelegationResult).
|
||||||
|
#[cfg(feature = "multi-agent")]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ButlerDelegationResponse {
|
||||||
|
request: String,
|
||||||
|
tasks: Vec<serde_json::Value>,
|
||||||
|
success: bool,
|
||||||
|
summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Butler delegates a user request to expert agents via the Director.
|
||||||
|
#[cfg(feature = "multi-agent")]
|
||||||
|
// @connected
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn butler_delegate_task(
|
||||||
|
state: State<'_, KernelState>,
|
||||||
|
request: String,
|
||||||
|
) -> Result<serde_json::Value, String> {
|
||||||
|
use zclaw_kernel::director::{Director, DirectorConfig, DirectorAgent, AgentRole};
|
||||||
|
|
||||||
|
let kernel_lock = state.lock().await;
|
||||||
|
let kernel = kernel_lock.as_ref()
|
||||||
|
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
|
||||||
|
|
||||||
|
// Create a Director for this delegation
|
||||||
|
let director = Director::new(DirectorConfig::default());
|
||||||
|
|
||||||
|
// Register active agents from kernel as experts
|
||||||
|
let agents = kernel.list_agents();
|
||||||
|
for agent in agents {
|
||||||
|
let persona = agent.system_prompt.clone()
|
||||||
|
.or(agent.soul.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let director_agent = DirectorAgent::new(
|
||||||
|
agent.id.clone(),
|
||||||
|
agent.name.clone(),
|
||||||
|
AgentRole::Expert,
|
||||||
|
persona,
|
||||||
|
);
|
||||||
|
director.register_agent(director_agent).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(kernel_lock);
|
||||||
|
|
||||||
|
let result = director.butler_delegate(&request).await
|
||||||
|
.map_err(|e| format!("Butler delegation failed: {}", e))?;
|
||||||
|
|
||||||
|
// Convert to JSON-serializable response
|
||||||
|
let tasks: Vec<serde_json::Value> = result.tasks.iter().map(|t| {
|
||||||
|
serde_json::json!({
|
||||||
|
"id": t.id,
|
||||||
|
"description": t.description,
|
||||||
|
"category": t.category,
|
||||||
|
"priority": t.priority,
|
||||||
|
"status": match t.status {
|
||||||
|
zclaw_kernel::director::ExpertTaskStatus::Pending => "pending",
|
||||||
|
zclaw_kernel::director::ExpertTaskStatus::Assigned => "assigned",
|
||||||
|
zclaw_kernel::director::ExpertTaskStatus::InProgress => "in_progress",
|
||||||
|
zclaw_kernel::director::ExpertTaskStatus::Completed => "completed",
|
||||||
|
zclaw_kernel::director::ExpertTaskStatus::Failed => "failed",
|
||||||
|
},
|
||||||
|
"assigned_expert": t.assigned_expert.as_ref().map(|e| serde_json::json!({
|
||||||
|
"id": e.id.to_string(),
|
||||||
|
"name": e.name,
|
||||||
|
"role": e.role.as_str(),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"request": result.request,
|
||||||
|
"tasks": tasks,
|
||||||
|
"success": result.success,
|
||||||
|
"summary": result.summary,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ pub fn run() {
|
|||||||
kernel_commands::a2a::agent_a2a_discover,
|
kernel_commands::a2a::agent_a2a_discover,
|
||||||
#[cfg(feature = "multi-agent")]
|
#[cfg(feature = "multi-agent")]
|
||||||
kernel_commands::a2a::agent_a2a_delegate_task,
|
kernel_commands::a2a::agent_a2a_delegate_task,
|
||||||
|
#[cfg(feature = "multi-agent")]
|
||||||
|
kernel_commands::a2a::butler_delegate_task,
|
||||||
|
|
||||||
// Pipeline commands (DSL-based workflows)
|
// Pipeline commands (DSL-based workflows)
|
||||||
pipeline_commands::discovery::pipeline_list,
|
pipeline_commands::discovery::pipeline_list,
|
||||||
|
|||||||
Reference in New Issue
Block a user