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
- 新增 SaaS industry 模块 (types/service/handlers/mod/builtin) - 4 行业内置配置: healthcare/education/garment/ecommerce - 数据库迁移: industries + account_industries 表 - 8 个 API 端点 (CRUD + 用户行业关联) - ButlerRouter 改造: 支持 IndustryKeywordConfig 动态注入 - 12 个测试全通过 (含动态行业分类测试)
459 lines
18 KiB
Rust
459 lines
18 KiB
Rust
//! Kernel - central coordinator
|
|
|
|
mod adapters;
|
|
mod agents;
|
|
mod messaging;
|
|
mod skills;
|
|
mod hands;
|
|
mod triggers;
|
|
mod approvals;
|
|
mod orchestration;
|
|
#[cfg(feature = "multi-agent")]
|
|
mod a2a;
|
|
|
|
use std::sync::Arc;
|
|
use tokio::sync::{broadcast, Mutex};
|
|
use zclaw_types::{Event, Result, AgentState};
|
|
|
|
#[cfg(feature = "multi-agent")]
|
|
use zclaw_types::AgentId;
|
|
#[cfg(feature = "multi-agent")]
|
|
use zclaw_protocols::A2aRouter;
|
|
|
|
use crate::registry::AgentRegistry;
|
|
use crate::capabilities::CapabilityManager;
|
|
use crate::events::EventBus;
|
|
use crate::config::KernelConfig;
|
|
use zclaw_memory::MemoryStore;
|
|
use zclaw_runtime::{LlmDriver, ToolRegistry, tool::SkillExecutor};
|
|
use zclaw_skills::SkillRegistry;
|
|
use zclaw_hands::{HandRegistry, hands::{BrowserHand, SlideshowHand, SpeechHand, QuizHand, WhiteboardHand, ResearcherHand, CollectorHand, ClipHand, TwitterHand, quiz::LlmQuizGenerator}};
|
|
|
|
pub use adapters::KernelSkillExecutor;
|
|
pub use messaging::ChatModeConfig;
|
|
|
|
/// The ZCLAW Kernel
|
|
pub struct Kernel {
|
|
config: KernelConfig,
|
|
registry: AgentRegistry,
|
|
capabilities: CapabilityManager,
|
|
events: EventBus,
|
|
memory: Arc<MemoryStore>,
|
|
driver: Arc<dyn LlmDriver>,
|
|
llm_completer: Arc<dyn zclaw_skills::LlmCompleter>,
|
|
skills: Arc<SkillRegistry>,
|
|
skill_executor: Arc<KernelSkillExecutor>,
|
|
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<zclaw_types::HandRunId, Arc<std::sync::atomic::AtomicBool>>>,
|
|
/// Shared memory storage backend for Growth system
|
|
viking: Arc<zclaw_runtime::VikingAdapter>,
|
|
/// Optional LLM driver for memory extraction (set by Tauri desktop layer)
|
|
extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>,
|
|
/// MCP tool adapters — shared with Tauri MCP manager, updated dynamically
|
|
mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>,
|
|
/// A2A router for inter-agent messaging (gated by multi-agent feature)
|
|
#[cfg(feature = "multi-agent")]
|
|
a2a_router: Arc<A2aRouter>,
|
|
/// Per-agent A2A inbox receivers (supports re-queuing non-matching messages)
|
|
#[cfg(feature = "multi-agent")]
|
|
a2a_inboxes: Arc<dashmap::DashMap<AgentId, Arc<Mutex<adapters::AgentInbox>>>>,
|
|
}
|
|
|
|
impl Kernel {
|
|
/// Boot the kernel with the given configuration
|
|
pub async fn boot(config: KernelConfig) -> Result<Self> {
|
|
// Initialize memory store
|
|
let memory = Arc::new(MemoryStore::new(&config.database_url).await?);
|
|
|
|
// Initialize driver based on config
|
|
let driver = config.create_driver()?;
|
|
|
|
// Initialize subsystems
|
|
let registry = AgentRegistry::new();
|
|
let capabilities = CapabilityManager::new();
|
|
let events = EventBus::new();
|
|
|
|
// Initialize skill registry
|
|
let skills = Arc::new(SkillRegistry::new());
|
|
|
|
// Scan skills directory if configured
|
|
if let Some(ref skills_dir) = config.skills_dir {
|
|
if skills_dir.exists() {
|
|
skills.add_skill_dir(skills_dir.clone()).await?;
|
|
}
|
|
}
|
|
|
|
// Initialize hand registry with built-in hands
|
|
let hands = Arc::new(HandRegistry::new());
|
|
let quiz_model = config.model().to_string();
|
|
let quiz_generator = Arc::new(LlmQuizGenerator::new(driver.clone(), quiz_model));
|
|
hands.register(Arc::new(BrowserHand::new())).await;
|
|
hands.register(Arc::new(SlideshowHand::new())).await;
|
|
hands.register(Arc::new(SpeechHand::new())).await;
|
|
hands.register(Arc::new(QuizHand::with_generator(quiz_generator))).await;
|
|
hands.register(Arc::new(WhiteboardHand::new())).await;
|
|
hands.register(Arc::new(ResearcherHand::new())).await;
|
|
hands.register(Arc::new(CollectorHand::new())).await;
|
|
hands.register(Arc::new(ClipHand::new())).await;
|
|
hands.register(Arc::new(TwitterHand::new())).await;
|
|
|
|
// Create skill executor
|
|
let skill_executor = Arc::new(KernelSkillExecutor::new(skills.clone(), driver.clone()));
|
|
|
|
// Create LLM completer for skill system (shared with skill_executor)
|
|
let llm_completer: Arc<dyn zclaw_skills::LlmCompleter> =
|
|
Arc::new(adapters::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());
|
|
|
|
// Initialize Growth system — shared VikingAdapter for memory storage
|
|
let viking = Arc::new(zclaw_runtime::VikingAdapter::in_memory());
|
|
|
|
// Restore persisted agents with their runtime state
|
|
let persisted = memory.list_agents_with_runtime().await?;
|
|
for (agent, state_str, msg_count) in persisted {
|
|
let state = match state_str.as_str() {
|
|
"running" => AgentState::Running,
|
|
"suspended" => AgentState::Suspended,
|
|
_ => AgentState::Terminated,
|
|
};
|
|
// Only auto-resume agents that were running; suspended/terminated stay as-is
|
|
let restored_state = if state == AgentState::Running {
|
|
AgentState::Running
|
|
} else {
|
|
state
|
|
};
|
|
registry.register_with_runtime(agent, restored_state, msg_count);
|
|
}
|
|
|
|
// 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,
|
|
capabilities,
|
|
events,
|
|
memory,
|
|
driver,
|
|
llm_completer,
|
|
skills,
|
|
skill_executor,
|
|
hands,
|
|
trigger_manager,
|
|
pending_approvals: Arc::new(Mutex::new(Vec::new())),
|
|
running_hand_runs: Arc::new(dashmap::DashMap::new()),
|
|
viking,
|
|
extraction_driver: None,
|
|
mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())), #[cfg(feature = "multi-agent")]
|
|
a2a_router,
|
|
#[cfg(feature = "multi-agent")]
|
|
a2a_inboxes: Arc::new(dashmap::DashMap::new()),
|
|
})
|
|
}
|
|
|
|
/// Create a tool registry with built-in tools + MCP tools.
|
|
/// When `subagent_enabled` is false, TaskTool is excluded to prevent
|
|
/// the LLM from attempting sub-agent delegation in non-Ultra modes.
|
|
pub(crate) fn create_tool_registry(&self, subagent_enabled: bool) -> ToolRegistry {
|
|
let mut tools = ToolRegistry::new();
|
|
zclaw_runtime::tool::builtin::register_builtin_tools(&mut tools);
|
|
|
|
// Register TaskTool only when sub-agent mode is enabled (Ultra mode)
|
|
if subagent_enabled {
|
|
let task_tool = zclaw_runtime::tool::builtin::TaskTool::new(
|
|
self.driver.clone(),
|
|
self.memory.clone(),
|
|
self.config.model(),
|
|
);
|
|
tools.register(Box::new(task_tool));
|
|
}
|
|
|
|
// Register MCP tools (dynamically updated by Tauri MCP manager)
|
|
if let Ok(adapters) = self.mcp_adapters.read() {
|
|
for adapter in adapters.iter() {
|
|
let wrapper = zclaw_runtime::tool::builtin::McpToolWrapper::new(
|
|
std::sync::Arc::new(adapter.clone())
|
|
);
|
|
tools.register(Box::new(wrapper));
|
|
}
|
|
}
|
|
|
|
tools
|
|
}
|
|
|
|
/// Create the middleware chain for the agent loop.
|
|
///
|
|
/// When middleware is configured, cross-cutting concerns (compaction, loop guard,
|
|
/// token calibration, etc.) are delegated to the chain. When no middleware is
|
|
/// registered, the legacy inline path in `AgentLoop` is used instead.
|
|
pub(crate) fn create_middleware_chain(&self) -> Option<zclaw_runtime::middleware::MiddlewareChain> {
|
|
let mut chain = zclaw_runtime::middleware::MiddlewareChain::new();
|
|
|
|
// Butler router — semantic skill routing context injection
|
|
{
|
|
use std::sync::Arc;
|
|
use zclaw_runtime::middleware::butler_router::{ButlerRouterBackend, RoutingHint};
|
|
use async_trait::async_trait;
|
|
use zclaw_skills::semantic_router::SemanticSkillRouter;
|
|
|
|
/// Adapter bridging `SemanticSkillRouter` (zclaw-skills) to `ButlerRouterBackend`.
|
|
/// Lives here in kernel because kernel depends on both zclaw-runtime and zclaw-skills.
|
|
struct SemanticRouterAdapter {
|
|
router: Arc<SemanticSkillRouter>,
|
|
}
|
|
|
|
impl SemanticRouterAdapter {
|
|
fn new(router: Arc<SemanticSkillRouter>) -> Self {
|
|
Self { router }
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ButlerRouterBackend for SemanticRouterAdapter {
|
|
async fn classify(&self, query: &str) -> Option<RoutingHint> {
|
|
let result: Option<_> = self.router.route(query).await;
|
|
result.map(|r| RoutingHint {
|
|
category: "semantic_skill".to_string(),
|
|
confidence: r.confidence,
|
|
skill_id: Some(r.skill_id),
|
|
domain_prompt: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Build semantic router from the skill registry (75 SKILL.md loaded at boot)
|
|
let semantic_router = SemanticSkillRouter::new_tf_idf_only(self.skills.clone());
|
|
let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router));
|
|
let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router(
|
|
Box::new(adapter)
|
|
);
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Data masking middleware — mask sensitive entities before any other processing
|
|
{
|
|
use std::sync::Arc;
|
|
let masker = Arc::new(zclaw_runtime::middleware::data_masking::DataMasker::new());
|
|
let mw = zclaw_runtime::middleware::data_masking::DataMaskingMiddleware::new(masker);
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Growth integration — shared VikingAdapter for memory middleware & compaction
|
|
let mut growth = zclaw_runtime::GrowthIntegration::new(self.viking.clone());
|
|
if let Some(ref driver) = self.extraction_driver {
|
|
growth = growth.with_llm_driver(driver.clone());
|
|
}
|
|
|
|
// Compaction middleware — only register when threshold > 0
|
|
let threshold = self.config.compaction_threshold();
|
|
if threshold > 0 {
|
|
use std::sync::Arc;
|
|
let mut growth_for_compaction = zclaw_runtime::GrowthIntegration::new(self.viking.clone());
|
|
if let Some(ref driver) = self.extraction_driver {
|
|
growth_for_compaction = growth_for_compaction.with_llm_driver(driver.clone());
|
|
}
|
|
let mw = zclaw_runtime::middleware::compaction::CompactionMiddleware::new(
|
|
threshold,
|
|
zclaw_runtime::CompactionConfig::default(),
|
|
Some(self.driver.clone()),
|
|
Some(growth_for_compaction),
|
|
);
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Memory middleware — auto-extract memories after conversations
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::memory::MemoryMiddleware::new(growth);
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Loop guard middleware
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::loop_guard::LoopGuardMiddleware::with_defaults();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Token calibration middleware
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::token_calibration::TokenCalibrationMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Skill index middleware — inject lightweight index instead of full descriptions
|
|
{
|
|
use std::sync::Arc;
|
|
let entries = self.skill_executor.list_skill_index();
|
|
if !entries.is_empty() {
|
|
let mw = zclaw_runtime::middleware::skill_index::SkillIndexMiddleware::new(entries);
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
}
|
|
|
|
// Title middleware — auto-generate conversation titles after first exchange
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::title::TitleMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Dangling tool repair — patch missing tool results before LLM calls
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::dangling_tool::DanglingToolMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Tool error middleware — format tool errors for LLM recovery
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::tool_error::ToolErrorMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Tool output guard — post-execution output sanitization checks
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::tool_output_guard::ToolOutputGuardMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Guardrail middleware — safety rules for tool calls
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::guardrail::GuardrailMiddleware::new(true)
|
|
.with_builtin_rules();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Sub-agent limit — cap concurrent sub-agent spawning
|
|
{
|
|
use std::sync::Arc;
|
|
let mw = zclaw_runtime::middleware::subagent_limit::SubagentLimitMiddleware::new();
|
|
chain.register(Arc::new(mw));
|
|
}
|
|
|
|
// Only return Some if we actually registered middleware
|
|
if chain.is_empty() {
|
|
None
|
|
} else {
|
|
tracing::info!("[Kernel] Middleware chain created with {} middlewares", chain.len());
|
|
Some(chain)
|
|
}
|
|
}
|
|
|
|
/// Subscribe to events
|
|
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
|
|
self.events.subscribe()
|
|
}
|
|
|
|
/// Shutdown the kernel
|
|
pub async fn shutdown(&self) -> Result<()> {
|
|
// Persist all agent runtime states before shutdown
|
|
let agents = self.registry.list();
|
|
for info in &agents {
|
|
let state_str = match info.state {
|
|
AgentState::Running => "running",
|
|
AgentState::Suspended => "suspended",
|
|
AgentState::Terminated => "terminated",
|
|
};
|
|
if let Err(e) = self.memory
|
|
.update_agent_runtime(&info.id, state_str, info.message_count as u64)
|
|
.await
|
|
{
|
|
tracing::warn!("[Kernel] Failed to persist agent {} state: {}", info.id, e);
|
|
}
|
|
}
|
|
tracing::info!("[Kernel] Persisted runtime state for {} agents", agents.len());
|
|
|
|
self.events.publish(Event::KernelShutdown);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the kernel configuration
|
|
pub fn config(&self) -> &KernelConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Get the LLM driver
|
|
pub fn driver(&self) -> Arc<dyn LlmDriver> {
|
|
self.driver.clone()
|
|
}
|
|
|
|
/// Replace the default in-memory VikingAdapter with a persistent one.
|
|
///
|
|
/// Called by the Tauri desktop layer after `Kernel::boot()` to bridge
|
|
/// the kernel's Growth system to the same SqliteStorage used by
|
|
/// viking_commands and intelligence_hooks.
|
|
pub fn set_viking(&mut self, viking: Arc<zclaw_runtime::VikingAdapter>) {
|
|
tracing::info!("[Kernel] Replacing in-memory VikingAdapter with persistent storage");
|
|
self.viking = viking;
|
|
}
|
|
|
|
/// Get a reference to the shared VikingAdapter
|
|
pub fn viking(&self) -> Arc<zclaw_runtime::VikingAdapter> {
|
|
self.viking.clone()
|
|
}
|
|
|
|
/// Set the LLM extraction driver for the Growth system.
|
|
///
|
|
/// Required for `MemoryMiddleware` to extract memories from conversations
|
|
/// via LLM analysis. If not set, memory extraction is silently skipped.
|
|
pub fn set_extraction_driver(&mut self, driver: Arc<dyn zclaw_runtime::LlmDriverForExtraction>) {
|
|
tracing::info!("[Kernel] Extraction driver configured for Growth system");
|
|
self.extraction_driver = Some(driver);
|
|
}
|
|
|
|
/// Get a reference to the shared MCP adapters list.
|
|
///
|
|
/// The Tauri MCP manager updates this list when services start/stop.
|
|
/// The kernel reads it during `create_tool_registry()` to inject MCP tools
|
|
/// into the LLM's available tools.
|
|
pub fn mcp_adapters(&self) -> Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>> {
|
|
self.mcp_adapters.clone()
|
|
}
|
|
|
|
/// Replace the MCP adapters with a shared Arc (from Tauri MCP manager).
|
|
///
|
|
/// Call this after boot to connect the kernel to the Tauri MCP manager's
|
|
/// adapter list. After this, MCP service start/stop will automatically
|
|
/// be reflected in the LLM's available tools.
|
|
pub fn set_mcp_adapters(&mut self, adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>) {
|
|
tracing::info!("[Kernel] MCP adapters bridge connected");
|
|
self.mcp_adapters = adapters;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ApprovalEntry {
|
|
pub id: String,
|
|
pub hand_id: String,
|
|
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
|
|
#[derive(Debug, Clone)]
|
|
pub struct MessageResponse {
|
|
pub content: String,
|
|
pub input_tokens: u32,
|
|
pub output_tokens: u32,
|
|
}
|