refactor(crates): kernel/generation module split + DeerFlow optimizations + middleware + dead code cleanup
- Split zclaw-kernel/kernel.rs (1486 lines) into 9 domain modules - Split zclaw-kernel/generation.rs (1080 lines) into 3 modules - Add DeerFlow-inspired middleware: DanglingTool, SubagentLimit, ToolError, ToolOutputGuard - Add PromptBuilder for structured system prompt assembly - Add FactStore (zclaw-memory) for persistent fact extraction - Add task builtin tool for agent task management - Driver improvements: Anthropic/OpenAI extended thinking, Gemini safety settings - Replace let _ = with proper log::warn! across SaaS handlers - Remove unused dependency (url) from zclaw-hands
This commit is contained in:
345
crates/zclaw-kernel/src/kernel/mod.rs
Normal file
345
crates/zclaw-kernel/src/kernel/mod.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
//! Kernel - central coordinator
|
||||
|
||||
mod adapters;
|
||||
mod agents;
|
||||
mod messaging;
|
||||
mod skills;
|
||||
mod hands;
|
||||
mod triggers;
|
||||
mod approvals;
|
||||
#[cfg(feature = "multi-agent")]
|
||||
mod a2a;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use zclaw_types::{Event, Result};
|
||||
|
||||
#[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>>,
|
||||
/// 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
|
||||
let persisted = memory.list_agents().await?;
|
||||
for agent in persisted {
|
||||
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,
|
||||
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,
|
||||
#[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
|
||||
pub(crate) fn create_tool_registry(&self) -> ToolRegistry {
|
||||
let mut tools = ToolRegistry::new();
|
||||
zclaw_runtime::tool::builtin::register_builtin_tools(&mut tools);
|
||||
|
||||
// Register TaskTool with driver and memory for sub-agent delegation
|
||||
let task_tool = zclaw_runtime::tool::builtin::TaskTool::new(
|
||||
self.driver.clone(),
|
||||
self.memory.clone(),
|
||||
self.config.model(),
|
||||
);
|
||||
tools.register(Box::new(task_tool));
|
||||
|
||||
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();
|
||||
|
||||
// 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<()> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
Reference in New Issue
Block a user