Prompt Tiers
The system prompt is built in three tiers, so the provider's prompt cache keeps working for the whole session.
The system prompt has three tiers, like Hermes. The first two are marked as cache breakpoints, so the model provider can reuse them across turns. Only the last tier changes between turns.
pub struct PromptParts {
/// Stable tier: the profile's SOUL.md.
pub soul: String,
/// Context tier, frozen when the session starts.
pub context: String,
}
pub fn build_system(parts: &PromptParts, now: &str) -> Vec<SystemBlock>;Stable: SOUL.md
The persona. Each profile has a SOUL.md; edit it to change how the agent talks and what it focuses on. New profiles start with:
You are a helpful personal agent. Be direct and brief. Use your tools when they help, and say plainly when you can't do something.Context: frozen per session
At the first turn of a session, CommonOS gathers the context tier from two places and stores it on the session:
- The skills index. One line per skill, from the
nameanddescriptionin eachskills/<name>/SKILL.md. - Every plugin’s
ContextSource. The Memory plugin adds both memory stores.
Skills:
- cooking: Plan weeknight meals
MEMORY (your notes) — 1% used, 14/2200 chars
likes oat milk
USER (about the person) — 0% used, 0/1375 chars
(empty)Later turns in the same session reuse this text exactly. When the agent saves a new memory, it shows up in the next session, not the current one. That is the price of a stable prompt prefix, and it is the same trade Hermes makes.
Adding context from a plugin
#[async_trait]
pub trait ContextSource: Send + Sync {
fn name(&self) -> &str;
async fn context(&self, profile_id: &str) -> anyhow::Result<String>;
}Register it with ctx.context(...) in your plugin’s register. It is called once per new session.
Volatile: the current time
The last block is Current time: <RFC 3339 timestamp>. It is not cached.
Per-turn context from hooks
A pre_llm_call hook can add context for one turn by returning Modify with {"context": "..."} (or a plain string). The text is put in front of the user message for the model only. It is not stored, and it does not touch the cached tiers. If the hook fails, the turn continues without it.
How caching is applied
With the Anthropic provider, the stable and context blocks carry cache_control: {"type": "ephemeral"}. The volatile block does not. Empty blocks are left out, so a profile with no skills and no plugin context sends only the persona and the time. Cache reads and writes show up in each call’s usage, and CommonBill prices them separately.