diff --git a/crates/zclaw-kernel/src/lib.rs b/crates/zclaw-kernel/src/lib.rs new file mode 100644 index 0000000..1cb067d --- /dev/null +++ b/crates/zclaw-kernel/src/lib.rs @@ -0,0 +1,15 @@ +//! ZCLAW Kernel +//! +//! Central coordinator for all ZCLAW subsystems. + +mod kernel; +mod registry; +mod capabilities; +mod events; +pub mod config; + +pub use kernel::*; +pub use registry::*; +pub use capabilities::*; +pub use events::*; +pub use config::*; diff --git a/crates/zclaw-kernel/src/registry.rs b/crates/zclaw-kernel/src/registry.rs new file mode 100644 index 0000000..f78394f --- /dev/null +++ b/crates/zclaw-kernel/src/registry.rs @@ -0,0 +1,92 @@ +//! Agent registry + +use dashmap::DashMap; +use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState}; +use chrono::Utc; + +/// In-memory registry of active agents +pub struct AgentRegistry { + agents: DashMap, + states: DashMap, + created_at: DashMap>, +} + +impl AgentRegistry { + pub fn new() -> Self { + Self { + agents: DashMap::new(), + states: DashMap::new(), + created_at: DashMap::new(), + } + } + + /// Register an agent + pub fn register(&self, config: AgentConfig) { + let id = config.id; + self.agents.insert(id, config); + self.states.insert(id, AgentState::Running); + self.created_at.insert(id, Utc::now()); + } + + /// Unregister an agent + pub fn unregister(&self, id: &AgentId) { + self.agents.remove(id); + self.states.remove(id); + self.created_at.remove(id); + } + + /// Get an agent by ID + pub fn get(&self, id: &AgentId) -> Option { + self.agents.get(id).map(|r| r.clone()) + } + + /// Get agent info + pub fn get_info(&self, id: &AgentId) -> Option { + let config = self.agents.get(id)?; + let state = self.states.get(id).map(|s| *s).unwrap_or(AgentState::Terminated); + let created_at = self.created_at.get(id).map(|t| *t).unwrap_or_else(Utc::now); + + Some(AgentInfo { + id: *id, + name: config.name.clone(), + description: config.description.clone(), + model: config.model.model.clone(), + provider: config.model.provider.clone(), + state, + message_count: 0, // TODO: Track this + created_at, + updated_at: Utc::now(), + }) + } + + /// List all agents + pub fn list(&self) -> Vec { + self.agents.iter() + .filter_map(|entry| { + let id = entry.key(); + self.get_info(id) + }) + .collect() + } + + /// Update agent state + pub fn set_state(&self, id: &AgentId, state: AgentState) { + self.states.insert(*id, state); + } + + /// Get agent state + pub fn get_state(&self, id: &AgentId) -> AgentState { + self.states.get(id).map(|s| *s).unwrap_or(AgentState::Terminated) + } + + /// Count active agents + pub fn count(&self) -> usize { + self.agents.len() + } +} + +impl Default for AgentRegistry { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/zclaw-types/src/config.rs b/crates/zclaw-types/src/config.rs new file mode 100644 index 0000000..9f3d9a4 --- /dev/null +++ b/crates/zclaw-types/src/config.rs @@ -0,0 +1,129 @@ +//! Configuration types + +use serde::{Deserialize, Serialize}; + +/// Kernel configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KernelConfig { + /// Database URL (SQLite or PostgreSQL) + pub database_url: String, + + /// Default LLM provider + pub default_provider: String, + + /// Default model + pub default_model: String, + + /// Maximum tokens per response + #[serde(default = "default_max_tokens")] + pub max_tokens: u32, + + /// Default temperature + #[serde(default = "default_temperature")] + pub temperature: f32, + + /// Enable debug logging + #[serde(default)] + pub debug: bool, +} + +fn default_max_tokens() -> u32 { + 4096 +} + +fn default_temperature() -> f32 { + 0.7 +} + +impl Default for KernelConfig { + fn default() -> Self { + Self { + database_url: "sqlite::memory:".to_string(), + default_provider: "anthropic".to_string(), + default_model: "claude-sonnet-4-20250514".to_string(), + max_tokens: default_max_tokens(), + temperature: default_temperature(), + debug: false, + } + } +} + +/// Model configuration for an agent +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ModelConfig { + /// Provider name (anthropic, openai, gemini, ollama, etc.) + pub provider: String, + /// Model identifier + pub model: String, + /// API key environment variable name + #[serde(default)] + pub api_key_env: Option, + /// Custom base URL (for OpenAI-compatible providers) + #[serde(default)] + pub base_url: Option, +} + +impl Default for ModelConfig { + fn default() -> Self { + Self { + provider: "anthropic".to_string(), + model: "claude-sonnet-4-20250514".to_string(), + api_key_env: Some("ANTHROPIC_API_KEY".to_string()), + base_url: None, + } + } +} + +impl ModelConfig { + pub fn anthropic(model: impl Into) -> Self { + Self { + provider: "anthropic".to_string(), + model: model.into(), + api_key_env: Some("ANTHROPIC_API_KEY".to_string()), + base_url: None, + } + } + + pub fn openai(model: impl Into) -> Self { + Self { + provider: "openai".to_string(), + model: model.into(), + api_key_env: Some("OPENAI_API_KEY".to_string()), + base_url: None, + } + } + + pub fn gemini(model: impl Into) -> Self { + Self { + provider: "gemini".to_string(), + model: model.into(), + api_key_env: Some("GEMINI_API_KEY".to_string()), + base_url: None, + } + } + + pub fn ollama(model: impl Into) -> Self { + Self { + provider: "ollama".to_string(), + model: model.into(), + api_key_env: None, + base_url: Some("http://localhost:11434".to_string()), + } + } + + pub fn openai_compatible(model: impl Into, base_url: impl Into) -> Self { + Self { + provider: "openai".to_string(), + model: model.into(), + api_key_env: None, + base_url: Some(base_url.into()), + } + } + + /// Check if this uses the same driver as another config + pub fn same_driver(&self, other: &ModelConfig) -> bool { + self.provider == other.provider + && self.api_key_env == other.api_key_env + && self.base_url == other.base_url + } +} diff --git a/crates/zclaw-types/src/tool.rs b/crates/zclaw-types/src/tool.rs new file mode 100644 index 0000000..d9cbc9f --- /dev/null +++ b/crates/zclaw-types/src/tool.rs @@ -0,0 +1,90 @@ +//! Tool definition types + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// Tool definition for LLM function calling +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolDefinition { + /// Tool name (unique identifier) + pub name: String, + /// Human-readable description + pub description: String, + /// JSON Schema for input parameters + pub input_schema: Value, +} + +impl ToolDefinition { + pub fn new(name: impl Into, description: impl Into, schema: Value) -> Self { + Self { + name: name.into(), + description: description.into(), + input_schema: schema, + } + } + + /// Create a simple tool with string parameters + pub fn simple(name: impl Into, description: impl Into, params: &[&str]) -> Self { + let properties: Value = params + .iter() + .map(|p| { + let s = p.to_string(); + (s.clone(), serde_json::json!({"type": "string"})) + }) + .collect(); + + let required: Vec<&str> = params.to_vec(); + + Self { + name: name.into(), + description: description.into(), + input_schema: serde_json::json!({ + "type": "object", + "properties": properties, + "required": required + }), + } + } +} + +/// Tool execution result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolResult { + /// Whether execution succeeded + pub success: bool, + /// Output data + pub output: Value, + /// Error message if failed + pub error: Option, +} + +impl ToolResult { + pub fn success(output: Value) -> Self { + Self { + success: true, + output, + error: None, + } + } + + pub fn error(message: impl Into) -> Self { + Self { + success: false, + output: Value::Null, + error: Some(message.into()), + } + } +} + +/// Built-in tool names +pub mod builtin_tools { + pub const FILE_READ: &str = "file_read"; + pub const FILE_WRITE: &str = "file_write"; + pub const FILE_LIST: &str = "file_list"; + pub const SHELL_EXEC: &str = "shell_exec"; + pub const WEB_FETCH: &str = "web_fetch"; + pub const WEB_SEARCH: &str = "web_search"; + pub const MEMORY_STORE: &str = "memory_store"; + pub const MEMORY_RECALL: &str = "memory_recall"; + pub const MEMORY_SEARCH: &str = "memory_search"; +} diff --git a/desktop/src-tauri/src/kernel_commands.rs b/desktop/src-tauri/src/kernel_commands.rs new file mode 100644 index 0000000..767b87e --- /dev/null +++ b/desktop/src-tauri/src/kernel_commands.rs @@ -0,0 +1,276 @@ +//! ZCLAW Kernel commands for Tauri +//! +//! These commands provide direct access to the internal ZCLAW Kernel, +//! eliminating the need for external OpenFang process. + +use std::sync::Arc; +use tauri::{AppHandle, Manager, State}; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +use zclaw_kernel::Kernel; +use zclaw_types::{AgentConfig, AgentId, AgentInfo, AgentState}; + +/// Kernel state wrapper for Tauri +pub type KernelState = Arc>>; + +/// Agent creation request +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAgentRequest { + /// Agent name + pub name: String, + /// Agent description + #[serde(default)] + pub description: Option, + /// System prompt + #[serde(default)] + pub system_prompt: Option, + /// Model provider + #[serde(default = "default_provider")] + pub provider: String, + /// Model identifier + #[serde(default = "default_model")] + pub model: String, + /// Max tokens + #[serde(default = "default_max_tokens")] + pub max_tokens: u32, + /// Temperature + #[serde(default = "default_temperature")] + pub temperature: f32, +} + +fn default_provider() -> String { "anthropic".to_string() } +fn default_model() -> String { "claude-sonnet-4-20250514".to_string() } +fn default_max_tokens() -> u32 { 4096 } +fn default_temperature() -> f32 { 0.7 } + +/// Agent creation response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateAgentResponse { + pub id: String, + pub name: String, + pub state: String, +} + +/// Chat request +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatRequest { + /// Agent ID + pub agent_id: String, + /// Message content + pub message: String, +} + +/// Chat response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChatResponse { + pub content: String, + pub input_tokens: u32, + pub output_tokens: u32, +} + +/// Kernel status response +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KernelStatusResponse { + pub initialized: bool, + pub agent_count: usize, + pub database_url: Option, + pub default_provider: Option, + pub default_model: Option, +} + +/// Initialize the internal ZCLAW Kernel +#[tauri::command] +pub async fn kernel_init( + state: State<'_, KernelState>, +) -> Result { + let mut kernel_lock = state.lock().await; + + if kernel_lock.is_some() { + let kernel = kernel_lock.as_ref().unwrap(); + return Ok(KernelStatusResponse { + initialized: true, + agent_count: kernel.list_agents().len(), + database_url: None, + default_provider: Some("anthropic".to_string()), + default_model: Some("claude-sonnet-4-20250514".to_string()), + }); + } + + // Load configuration + let config = zclaw_kernel::config::KernelConfig::default(); + + // Boot kernel + let kernel = Kernel::boot(config.clone()) + .await + .map_err(|e| format!("Failed to initialize kernel: {}", e))?; + + let agent_count = kernel.list_agents().len(); + + *kernel_lock = Some(kernel); + + Ok(KernelStatusResponse { + initialized: true, + agent_count, + database_url: Some(config.database_url), + default_provider: Some(config.default_provider), + default_model: Some(config.default_model), + }) +} + +/// Get kernel status +#[tauri::command] +pub async fn kernel_status( + state: State<'_, KernelState>, +) -> Result { + let kernel_lock = state.lock().await; + + match kernel_lock.as_ref() { + Some(kernel) => Ok(KernelStatusResponse { + initialized: true, + agent_count: kernel.list_agents().len(), + database_url: None, + default_provider: Some("anthropic".to_string()), + default_model: Some("claude-sonnet-4-20250514".to_string()), + }), + None => Ok(KernelStatusResponse { + initialized: false, + agent_count: 0, + database_url: None, + default_provider: None, + default_model: None, + }), + } +} + +/// Shutdown the kernel +#[tauri::command] +pub async fn kernel_shutdown( + state: State<'_, KernelState>, +) -> Result<(), String> { + let mut kernel_lock = state.lock().await; + + if let Some(kernel) = kernel_lock.take() { + kernel.shutdown().await.map_err(|e| e.to_string())?; + } + + Ok(()) +} + +/// Create a new agent +#[tauri::command] +pub async fn agent_create( + state: State<'_, KernelState>, + request: CreateAgentRequest, +) -> Result { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + // Build agent config + let config = AgentConfig::new(&request.name) + .with_description(request.description.unwrap_or_default()) + .with_system_prompt(request.system_prompt.unwrap_or_default()) + .with_model(zclaw_types::ModelConfig { + provider: request.provider, + model: request.model, + api_key_env: None, + base_url: None, + }) + .with_max_tokens(request.max_tokens) + .with_temperature(request.temperature); + + let id = kernel.spawn_agent(config) + .await + .map_err(|e| format!("Failed to create agent: {}", e))?; + + Ok(CreateAgentResponse { + id: id.to_string(), + name: request.name, + state: "running".to_string(), + }) +} + +/// List all agents +#[tauri::command] +pub async fn agent_list( + state: State<'_, KernelState>, +) -> Result, String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + Ok(kernel.list_agents()) +} + +/// Get agent info +#[tauri::command] +pub async fn agent_get( + state: State<'_, KernelState>, + agent_id: String, +) -> Result, String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + let id: AgentId = agent_id.parse() + .map_err(|_| "Invalid agent ID format".to_string())?; + + Ok(kernel.get_agent(&id)) +} + +/// Delete an agent +#[tauri::command] +pub async fn agent_delete( + state: State<'_, KernelState>, + agent_id: String, +) -> Result<(), String> { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + let id: AgentId = agent_id.parse() + .map_err(|_| "Invalid agent ID format".to_string())?; + + kernel.kill_agent(&id) + .await + .map_err(|e| format!("Failed to delete agent: {}", e)) +} + +/// Send a message to an agent +#[tauri::command] +pub async fn agent_chat( + state: State<'_, KernelState>, + request: ChatRequest, +) -> Result { + let kernel_lock = state.lock().await; + + let kernel = kernel_lock.as_ref() + .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; + + let id: AgentId = request.agent_id.parse() + .map_err(|_| "Invalid agent ID format".to_string())?; + + let response = kernel.send_message(&id, request.message) + .await + .map_err(|e| format!("Chat failed: {}", e))?; + + Ok(ChatResponse { + content: response.content, + input_tokens: response.input_tokens, + output_tokens: response.output_tokens, + }) +} + +/// Create the kernel state for Tauri +pub fn create_kernel_state() -> KernelState { + Arc::new(Mutex::new(None)) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 276e433..ebadf44 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -24,6 +24,9 @@ mod memory_commands; // Intelligence Layer (migrated from frontend lib/) mod intelligence; +// Internal ZCLAW Kernel commands (replaces external OpenFang process) +mod kernel_commands; + use serde::Serialize; use serde_json::{json, Value}; use std::fs; @@ -1308,6 +1311,9 @@ pub fn run() { let reflection_state: intelligence::ReflectionEngineState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::ReflectionEngine::new(None))); let identity_state: intelligence::IdentityManagerState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::AgentIdentityManager::new())); + // Initialize internal ZCLAW Kernel state + let kernel_state = kernel_commands::create_kernel_state(); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(browser_state) @@ -1315,7 +1321,17 @@ pub fn run() { .manage(heartbeat_state) .manage(reflection_state) .manage(identity_state) + .manage(kernel_state) .invoke_handler(tauri::generate_handler![ + // Internal ZCLAW Kernel commands (preferred) + kernel_commands::kernel_init, + kernel_commands::kernel_status, + kernel_commands::kernel_shutdown, + kernel_commands::agent_create, + kernel_commands::agent_list, + kernel_commands::agent_get, + kernel_commands::agent_delete, + kernel_commands::agent_chat, // OpenFang commands (new naming) openfang_status, openfang_start,