mecha-core — an agent harness for local models.
The library knows nothing about any particular CLI, UI, or project. It gives you four things and lets you wire them together:
- [
provider] — talk to a model (Anthropic, or anything OpenAI-shaped) - [
tool] — things the agent can do, native or [mcp]-backed - [
agent] — the loop that puts those together - [
session] / [batch] — persistence and fan-out around the loop
# async fn example() -> anyhow::Result<()> {
use mecha_core::{agent::Agent, agent::Conversation, config::Config};
use mecha_core::sandbox::Sandbox;
use mecha_core::tool::{ModeApprover, Registry, ToolCtx};
use std::sync::Arc;
let cfg = Config::load(&std::env::current_dir()?)?;
let (_, provider_cfg) = cfg.provider(None)?;
// How `shell` is confined. It decides that tool's declared capabilities,
// so it is built before the registry rather than consulted at call time.
let sandbox = Arc::new(Sandbox::new(cfg.sandbox.clone()));
let agent = Agent::new(
mecha_core::provider::build(provider_cfg)?,
Registry::new().with_builtins(&cfg.tools, sandbox),
Arc::new(ModeApprover { mode: cfg.tools.permission_mode }),
ToolCtx {
workspace: std::env::current_dir()?,
shell_timeout: std::time::Duration::from_secs(cfg.tools.shell_timeout_secs),
security: cfg.security.clone(),
..ToolCtx::default()
},
cfg.agent.clone(),
None,
)?;
// A conversation carries its own taint, so keeping it across turns keeps
// the trifecta interlock honest — see `agent::Conversation`.
let mut convo = Conversation::user("What changed in this repo today?");
let outcome = agent.run(&mut convo, None).await?;
println!("{}", outcome.text);
# Ok(())
# }