CommonAgent
The base agent every CommonOS agent call runs through. It owns the turn loop and the hook points; storage, prompts and approvals come in through traits.
CommonAgent is the heart of CommonOS. It takes one message, runs the model, calls tools, asks for approval when it must, fires hooks at every step, and returns a reply. Every agent call in CommonOS runs through it: a chat in the dashboard, a terminal chat, and every helper agent that delegate_task starts.
It lives in the common-agent crate and has no database or web code. Where it stores messages, how it asks a person for approval, and which model it calls are all traits you pass in. That makes it easy to test and easy to embed.
What it owns
- The turn loop. Model call, tool calls, repeat, until the model answers with text. The Turn Loop →
- Hook points. Twenty-two points with Hermes names and behavior. Hooks →
- Tools and risk. A registry of tools, each with a declared risk. Tools →
- Approvals. The built-in policy that turns risk into run, ask or block. Approvals →
- The system prompt. Three tiers with cache breakpoints. Prompt Tiers →
- Providers. The Anthropic Messages API and a mock. Providers →
What you plug in
| Trait | Job | In CommonOS | For tests |
|---|---|---|---|
Provider |
Calls the model | AnthropicProvider |
MockProvider |
Journal |
Stores sessions, turns, messages, tool calls | Postgres Store |
MemoryJournal |
Approver |
Asks a person to approve a tool call | Dashboard (ApprovalBroker), y/N in cos chat |
AutoApprover, DenyApprover |
Tool |
Something the model can call | Plugin tools | Your own |
Hook |
Runs at a hook point | approval_policy, live_feed, plugin hooks |
Your own |
ContextSource |
Adds a section to a session’s prompt | Memory, skills | — |
Embed it
This program builds an agent with one tool and one hook, and runs a turn against the scripted mock model. It needs no database and no network.
use std::sync::Arc;
use async_trait::async_trait;
use common_agent::provider::MockProvider;
use common_agent::{
new_id, parse_tags, ApprovalPolicy, CommonAgent, DenyApprover, Hook, HookBus, HookEvent,
HookPoint, HookResult, MemoryJournal, PromptParts, Risk, Tool, ToolContext, ToolError,
ToolRegistry, TurnConfig, TurnInfo, TurnRequest,
};
use serde_json::{json, Value};
use tokio_util::sync::CancellationToken;
struct Weather;
#[async_trait]
impl Tool for Weather {
fn name(&self) -> &str {
"weather"
}
fn description(&self) -> &str {
"Today's forecast for a city."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
})
}
fn risk(&self) -> Risk {
Risk::Read
}
async fn call(&self, _ctx: &ToolContext, args: Value) -> Result<String, ToolError> {
let city = args["city"].as_str().ok_or(ToolError::InvalidArgs("city".into()))?;
Ok(format!("{city}: 18°C, light rain"))
}
}
struct Usage;
#[async_trait]
impl Hook for Usage {
fn name(&self) -> &str {
"usage"
}
fn points(&self) -> &[HookPoint] {
&[HookPoint::PostApiRequest]
}
async fn call(&self, event: &HookEvent) -> anyhow::Result<HookResult> {
println!("model call: {}", event.payload["usage"]);
Ok(HookResult::Continue)
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let provider = MockProvider::scripted(vec![
MockProvider::tool_call("c1", "weather", json!({ "city": "Charleston" })),
MockProvider::text("Bring an umbrella: 18°C and light rain."),
]);
let mut tools = ToolRegistry::new();
tools.register(Arc::new(Weather));
let mut hooks = HookBus::default();
hooks.register(Arc::new(ApprovalPolicy));
hooks.register(Arc::new(Usage));
let agent = CommonAgent::new(
Arc::new(provider),
tools,
Arc::new(hooks),
Arc::new(MemoryJournal::default()),
Arc::new(DenyApprover),
TurnConfig::default(),
);
let outcome = agent
.run_turn(
TurnRequest {
turn: TurnInfo {
profile_id: "Household".into(),
session_id: new_id(),
turn_id: new_id(),
parent_turn_id: None,
tags: parse_tags(&["area:household"])?,
depth: 0,
},
user_text: "Do I need an umbrella?".into(),
prompt: PromptParts {
soul: "You are a helpful household agent.".into(),
context: String::new(),
},
model: MockProvider::MODEL.into(),
},
CancellationToken::new(),
)
.await?;
println!(
"{} after {} calls: {}",
outcome.status.as_str(),
outcome.iterations,
outcome.text.unwrap_or_default()
);
Ok(())
}model call: {"cache_read_tokens":0,"cache_write_tokens":0,"input_tokens":100,"output_tokens":20}
model call: {"cache_read_tokens":0,"cache_write_tokens":0,"input_tokens":100,"output_tokens":20}
completed after 2 calls: Bring an umbrella: 18°C and light rain.The model asked for the weather tool, got its result, then answered. That is two model calls in one turn, and the post_api_request hook saw both.
The public API
impl CommonAgent {
pub fn new(
provider: Arc<dyn Provider>,
tools: ToolRegistry,
hooks: Arc<HookBus>,
journal: Arc<dyn Journal>,
approver: Arc<dyn Approver>,
config: TurnConfig,
) -> Arc<Self>;
pub async fn run_turn(&self, request: TurnRequest, cancel: CancellationToken)
-> anyhow::Result<TurnOutcome>;
/// Run with a different tool set; child agents get a reduced one.
pub async fn run_turn_with(&self, request: TurnRequest, tools: &ToolRegistry, cancel: CancellationToken)
-> anyhow::Result<TurnOutcome>;
pub fn tools(&self) -> &ToolRegistry;
pub fn journal(&self) -> &Arc<dyn Journal>;
pub fn hooks(&self) -> &Arc<HookBus>;
}| Type | Fields |
|---|---|
TurnRequest |
turn: TurnInfo, user_text, prompt: PromptParts, model |
TurnOutcome |
turn, status: TurnStatus, text: Option<String>, iterations (model calls made) |
TurnConfig |
max_iterations (500), max_tokens (4096), approval_timeout (300 s) |
How CommonOS uses it
CommonOS builds one CommonAgent per profile at start-up. All of them share one hook bus and one Postgres journal. Each gets its own provider (from the profile’s config.toml) and its own tool registry (filtered by the profile’s tools list). The Dispatcher picks the right agent for each message and calls run_turn.