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.

StableSOUL.mdChanges only when you edit the persona.
ContextSkills index + every plugin’s ContextSourceGathered at the first turn of a session, then frozen.
VolatileCurrent time: …The only tier that changes between turns.
Rust
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:

SOUL.md
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 name and description in each skills/<name>/SKILL.md.
  • Every plugin’s ContextSource. The Memory plugin adds both memory stores.
A session's context tier
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

Rust
#[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.

Edit this page on GitHub

Esc