Turns, Sessions and Tags

The identity every turn carries, how conversations are stored, and how tags travel from a profile to every model call.

TurnInfo

Every hook, tool and journal call receives a TurnInfo. It says who is running and where.

Rust
pub struct TurnInfo {
    pub profile_id: String,
    pub session_id: Uuid,
    pub turn_id: Uuid,
    /// Set for a child (delegated) turn.
    pub parent_turn_id: Option<Uuid>,
    /// Resolved tags; a child copies its parent's.
    pub tags: Vec<Tag>,
    /// 0 for a top-level turn, 1 for a child.
    pub depth: u32,
}

IDs are UUID v7, so they sort by creation time.

Sessions

A session is one conversation with one profile. It holds the messages, the frozen prompt context, its own tags and, for a child agent, the session it was spawned from.

Rust
pub struct NewSession {
    pub profile_id: String,
    /// Where it came from: `web`, `cli`, `subagent`.
    pub source: String,
    pub title: Option<String>,
    pub tags: Vec<Tag>,
    pub parent_session_id: Option<Uuid>,
}

CommonOS titles a new session with the first 60 characters of its first message. A child session is titled with its goal.

Messages

A conversation is a list of messages. Each is stored as JSON with a role:

JSON
{ "role": "user", "text": "Do I need an umbrella?" }
{ "role": "assistant", "tool_calls": [{ "id": "c1", "name": "weather", "args": { "city": "Charleston" } }] }
{ "role": "tool_result", "call_id": "c1", "content": "Charleston: 18°C, light rain", "is_error": false }
{ "role": "assistant", "text": "Bring an umbrella: 18°C and light rain." }

Tags

A tag is a key:value label. Tags carry no meaning in the loop: CommonAgent copies them onto every turn and leaves interpretation to plugins. CommonBill uses them to group costs.

Format:

  • Key: lower-case letters, digits, _ and -.
  • Value: letters of either case, digits, _, - and ..
  • Exactly one :. No spaces.
Tag Valid?
area:household Yes
client:acme Yes
project:Site-v2.1 Yes
Area:x No: upper case in the key
area:has space No: space
a:b:c No: two colons
area No: no value

Where tags come from

A turn’s tags are the profile’s tags (from config.toml) plus the session’s own tags, sorted, without duplicates. A message that starts a new session can bring extra tags, for example from the dashboard’s WebSocket chat message.

Text
profile tags   area:work, team:core
session tags   project:site, area:work
turn tags      area:work, project:site, team:core

Child turns

TurnInfo::child makes the identity for a delegated turn: same profile, same tags, a new session, parent_turn_id set to the parent’s turn and depth + 1. Because tags are copied, anything keyed on tags rolls up through delegation. A helper agent working for client:acme is billed to client:acme.

The journal

The Journal trait is where the loop records what happens. CommonOS implements it with Postgres; MemoryJournal keeps everything in memory for tests.

Rust
#[async_trait]
pub trait Journal: Send + Sync {
    async fn create_session(&self, session: NewSession) -> anyhow::Result<Uuid>;
    /// Active messages of a session, oldest first.
    async fn load_history(&self, session_id: Uuid) -> anyhow::Result<Vec<Message>>;
    async fn start_turn(&self, turn: &TurnInfo) -> anyhow::Result<()>;
    async fn append_message(&self, turn: &TurnInfo, message: &Message) -> anyhow::Result<()>;
    async fn record_tool_call(&self, turn: &TurnInfo, record: &ToolCallRecord) -> anyhow::Result<()>;
    async fn finish_turn(&self, turn: &TurnInfo, status: TurnStatus) -> anyhow::Result<()>;
}

ToolCallRecord holds the call’s name, arguments, result, error flag, risk, duration and approval.

Edit this page on GitHub

Esc