feat: initial release — ZeroClaw v0.1.0

- 22 AI providers (OpenRouter, Anthropic, OpenAI, Mistral, etc.)
- 7 channels (CLI, Telegram, Discord, Slack, iMessage, Matrix, Webhook)
- 5-step onboarding wizard with Project Context personalization
- OpenClaw-aligned system prompt (SOUL.md, IDENTITY.md, USER.md, AGENTS.md, etc.)
- SQLite memory backend with auto-save
- Skills system with on-demand loading
- Security: autonomy levels, command allowlists, cost limits
- 532 tests passing, 0 clippy warnings
This commit is contained in:
argenis de la rosa 2026-02-13 12:19:14 -05:00
commit 05cb353f7f
71 changed files with 15757 additions and 0 deletions

182
src/agent/loop_.rs Normal file
View file

@ -0,0 +1,182 @@
use crate::config::Config;
use crate::memory::{self, Memory, MemoryCategory};
use crate::observability::{self, Observer, ObserverEvent};
use crate::providers::{self, Provider};
use crate::runtime;
use crate::security::SecurityPolicy;
use crate::tools;
use anyhow::Result;
use std::fmt::Write;
use std::sync::Arc;
use std::time::Instant;
/// Build context preamble by searching memory for relevant entries
async fn build_context(mem: &dyn Memory, user_msg: &str) -> String {
let mut context = String::new();
// Pull relevant memories for this message
if let Ok(entries) = mem.recall(user_msg, 5).await {
if !entries.is_empty() {
context.push_str("[Memory context]\n");
for entry in &entries {
let _ = writeln!(context, "- {}: {}", entry.key, entry.content);
}
context.push('\n');
}
}
context
}
#[allow(clippy::too_many_lines)]
pub async fn run(
config: Config,
message: Option<String>,
provider_override: Option<String>,
model_override: Option<String>,
temperature: f64,
) -> Result<()> {
// ── Wire up agnostic subsystems ──────────────────────────────
let observer: Arc<dyn Observer> =
Arc::from(observability::create_observer(&config.observability));
let _runtime = runtime::create_runtime(&config.runtime);
let security = Arc::new(SecurityPolicy::from_config(
&config.autonomy,
&config.workspace_dir,
));
// ── Memory (the brain) ────────────────────────────────────────
let mem: Arc<dyn Memory> =
Arc::from(memory::create_memory(&config.memory, &config.workspace_dir)?);
tracing::info!(backend = mem.name(), "Memory initialized");
// ── Tools (including memory tools) ────────────────────────────
let _tools = tools::all_tools(security, mem.clone());
// ── Resolve provider ─────────────────────────────────────────
let provider_name = provider_override
.as_deref()
.or(config.default_provider.as_deref())
.unwrap_or("openrouter");
let model_name = model_override
.as_deref()
.or(config.default_model.as_deref())
.unwrap_or("anthropic/claude-sonnet-4-20250514");
let provider: Box<dyn Provider> =
providers::create_provider(provider_name, config.api_key.as_deref())?;
observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.to_string(),
model: model_name.to_string(),
});
// ── Build system prompt from workspace MD files (OpenClaw framework) ──
let skills = crate::skills::load_skills(&config.workspace_dir);
let tool_descs: Vec<(&str, &str)> = vec![
("shell", "Execute terminal commands"),
("file_read", "Read file contents"),
("file_write", "Write file contents"),
("memory_store", "Save to memory"),
("memory_recall", "Search memory"),
("memory_forget", "Delete a memory entry"),
];
let system_prompt = crate::channels::build_system_prompt(
&config.workspace_dir,
model_name,
&tool_descs,
&skills,
);
// ── Execute ──────────────────────────────────────────────────
let start = Instant::now();
if let Some(msg) = message {
// Auto-save user message to memory
if config.memory.auto_save {
let _ = mem
.store("user_msg", &msg, MemoryCategory::Conversation)
.await;
}
// Inject memory context into user message
let context = build_context(mem.as_ref(), &msg).await;
let enriched = if context.is_empty() {
msg.clone()
} else {
format!("{context}{msg}")
};
let response = provider
.chat_with_system(Some(&system_prompt), &enriched, model_name, temperature)
.await?;
println!("{response}");
// Auto-save assistant response to daily log
if config.memory.auto_save {
let summary = if response.len() > 100 {
format!("{}...", &response[..100])
} else {
response.clone()
};
let _ = mem
.store("assistant_resp", &summary, MemoryCategory::Daily)
.await;
}
} else {
println!("🦀 ZeroClaw Interactive Mode");
println!("Type /quit to exit.\n");
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let cli = crate::channels::CliChannel::new();
// Spawn listener
let listen_handle = tokio::spawn(async move {
let _ = crate::channels::Channel::listen(&cli, tx).await;
});
while let Some(msg) = rx.recv().await {
// Auto-save conversation turns
if config.memory.auto_save {
let _ = mem
.store("user_msg", &msg.content, MemoryCategory::Conversation)
.await;
}
// Inject memory context into user message
let context = build_context(mem.as_ref(), &msg.content).await;
let enriched = if context.is_empty() {
msg.content.clone()
} else {
format!("{context}{}", msg.content)
};
let response = provider
.chat_with_system(Some(&system_prompt), &enriched, model_name, temperature)
.await?;
println!("\n{response}\n");
if config.memory.auto_save {
let summary = if response.len() > 100 {
format!("{}...", &response[..100])
} else {
response.clone()
};
let _ = mem
.store("assistant_resp", &summary, MemoryCategory::Daily)
.await;
}
}
listen_handle.abort();
}
let duration = start.elapsed();
observer.record_event(&ObserverEvent::AgentEnd {
duration,
tokens_used: None,
});
Ok(())
}