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

119
src/observability/log.rs Normal file
View file

@ -0,0 +1,119 @@
use super::traits::{Observer, ObserverEvent, ObserverMetric};
use tracing::info;
/// Log-based observer — uses tracing, zero external deps
pub struct LogObserver;
impl LogObserver {
pub fn new() -> Self {
Self
}
}
impl Observer for LogObserver {
fn record_event(&self, event: &ObserverEvent) {
match event {
ObserverEvent::AgentStart { provider, model } => {
info!(provider = %provider, model = %model, "agent.start");
}
ObserverEvent::AgentEnd {
duration,
tokens_used,
} => {
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
info!(duration_ms = ms, tokens = ?tokens_used, "agent.end");
}
ObserverEvent::ToolCall {
tool,
duration,
success,
} => {
let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
info!(tool = %tool, duration_ms = ms, success = success, "tool.call");
}
ObserverEvent::ChannelMessage { channel, direction } => {
info!(channel = %channel, direction = %direction, "channel.message");
}
ObserverEvent::HeartbeatTick => {
info!("heartbeat.tick");
}
ObserverEvent::Error { component, message } => {
info!(component = %component, error = %message, "error");
}
}
}
fn record_metric(&self, metric: &ObserverMetric) {
match metric {
ObserverMetric::RequestLatency(d) => {
let ms = u64::try_from(d.as_millis()).unwrap_or(u64::MAX);
info!(latency_ms = ms, "metric.request_latency");
}
ObserverMetric::TokensUsed(t) => {
info!(tokens = t, "metric.tokens_used");
}
ObserverMetric::ActiveSessions(s) => {
info!(sessions = s, "metric.active_sessions");
}
ObserverMetric::QueueDepth(d) => {
info!(depth = d, "metric.queue_depth");
}
}
}
fn name(&self) -> &str {
"log"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn log_observer_name() {
assert_eq!(LogObserver::new().name(), "log");
}
#[test]
fn log_observer_all_events_no_panic() {
let obs = LogObserver::new();
obs.record_event(&ObserverEvent::AgentStart {
provider: "openrouter".into(),
model: "claude-sonnet".into(),
});
obs.record_event(&ObserverEvent::AgentEnd {
duration: Duration::from_millis(500),
tokens_used: Some(100),
});
obs.record_event(&ObserverEvent::AgentEnd {
duration: Duration::ZERO,
tokens_used: None,
});
obs.record_event(&ObserverEvent::ToolCall {
tool: "shell".into(),
duration: Duration::from_millis(10),
success: false,
});
obs.record_event(&ObserverEvent::ChannelMessage {
channel: "telegram".into(),
direction: "outbound".into(),
});
obs.record_event(&ObserverEvent::HeartbeatTick);
obs.record_event(&ObserverEvent::Error {
component: "provider".into(),
message: "timeout".into(),
});
}
#[test]
fn log_observer_all_metrics_no_panic() {
let obs = LogObserver::new();
obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_secs(2)));
obs.record_metric(&ObserverMetric::TokensUsed(0));
obs.record_metric(&ObserverMetric::TokensUsed(u64::MAX));
obs.record_metric(&ObserverMetric::ActiveSessions(1));
obs.record_metric(&ObserverMetric::QueueDepth(999));
}
}