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.
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.
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:
{ "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.
profile tags area:work, team:core
session tags project:site, area:work
turn tags area:work, project:site, team:coreChild 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.
#[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.