use crate::agent::{
AgentId, Budget, ConversationId, CorrelationId, LogPosition, SdkInfo, SessionStatus, TokenUsage,
};
use crate::graph::SourceRef;
use crate::schema::Digest32;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LinkSurface {
Memory,
Kv,
GraphNode,
GraphEdge,
Projection,
Child,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LinkRelation {
Wrote,
Recalled,
Touched,
}
pub mod request {
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionGet {
pub stream: String,
pub id: ConversationId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionList {
pub stream: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<SessionStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<ConversationId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label_prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
pub limit: u32,
pub want_total: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionEvents {
pub stream: String,
pub id: ConversationId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
pub limit: u32,
pub fixed_frontier: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionState {
pub stream: String,
pub id: ConversationId,
pub history_limit: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionLinks {
pub stream: String,
pub id: ConversationId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub surface: Option<LinkSurface>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionSources {
pub stream: String,
pub id: ConversationId,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub lane_only: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionChanges {
pub stream: String,
pub after: u64,
pub limit: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub series: Option<u64>,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceFrontier {
pub topic_id: u32,
pub topic_generation: u64,
pub partition_id: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub folded: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub head: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retained_from: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GapReason {
ExpiredBeforeFold,
Pruned,
Truncated,
Rebuilding,
#[serde(other)]
Unrecognized,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceGap {
pub topic_id: u32,
pub topic_generation: u64,
pub partition_id: u32,
pub from: u64,
pub to: u64,
pub reason: GapReason,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PayloadRange {
pub topic_id: u32,
pub topic_generation: u64,
pub partition_id: u32,
pub first: u64,
pub last: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StateOutcome {
Applied,
Rejected,
Stale,
Duplicate,
#[serde(other)]
Unrecognized,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StateChange {
pub revision: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub op_id: Option<String>,
pub outcome: StateOutcome,
pub at: LogPosition,
pub broker_ts: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub old_digest: Option<Digest32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub new_digest: Option<Digest32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionFlags {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub label_truncated: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub overflow: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub events_truncated: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub lane_conflict: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub control_conflict: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub rebuilding: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub liveness_unknown: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionInfo {
pub stream: String,
pub id: ConversationId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub namespace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<ConversationId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub root: Option<ConversationId>,
pub status: SessionStatus,
pub idle: bool,
pub over_budget: bool,
pub pause_requested: bool,
pub cancel_requested: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ended_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub first_event_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_event_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_heartbeat_at: Option<u64>,
pub events: u64,
pub model_calls: u64,
pub tool_calls: u64,
pub tokens_in: u64,
pub tokens_out: u64,
pub cost_micros: u64,
pub errors: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget: Option<Budget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sdk: Option<SdkInfo>,
#[serde(default)]
pub flags: SessionFlags,
#[serde(default, skip_serializing_if = "is_zero")]
pub held: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frontier: Vec<SourceFrontier>,
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionPage {
pub items: Vec<SessionInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub searched: Option<u64>,
pub truncated: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub rebuilding: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SessionEvent {
pub at: SourceRef,
pub session: ConversationId,
pub broker_ts: u64,
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
pub display: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<AgentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub addressee: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub correlation: Option<CorrelationId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cause: Option<LogPosition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub after_end: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verified_actor: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub summary: BTreeMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SessionEventsPage {
pub items: Vec<SessionEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ranges: Vec<PayloadRange>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frontier: Vec<SourceFrontier>,
pub fixed_frontier: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gaps: Vec<SourceGap>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SessionStateView {
pub revision: u64,
pub document: serde_json::Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<StateChange>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frontier: Option<SourceFrontier>,
pub complete: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionLink {
pub surface: LinkSurface,
pub resource: String,
pub item: String,
pub relation: LinkRelation,
pub first: LogPosition,
pub last: LogPosition,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionLinksView {
pub links: Vec<SessionLink>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frontier: Vec<SourceFrontier>,
pub truncated: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionSources {
pub sources: Vec<SourceFrontier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lane: Option<(u32, u64, u32)>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionChangeRow {
pub seq: u64,
pub sessions: Vec<ConversationId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub positions: Vec<LogPosition>,
pub truncated: bool,
#[serde(default, skip_serializing_if = "SessionChangeOrigin::is_records")]
pub origin: SessionChangeOrigin,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionChangeOrigin {
#[default]
Records,
Links,
Maintenance,
Expiry,
}
impl SessionChangeOrigin {
pub fn is_records(&self) -> bool {
*self == Self::Records
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionChanges {
pub rows: Vec<SessionChangeRow>,
pub floor: u64,
pub resync: bool,
#[serde(default, skip_serializing_if = "is_zero_series")]
pub series: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SessionReply {
Ok(SessionOutcome),
Err(SessionError),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SessionOutcome {
Info(Box<SessionInfo>),
Page(SessionPage),
Events(SessionEventsPage),
State(SessionStateView),
Links(SessionLinksView),
Sources(SessionSources),
Changes(SessionChanges),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[non_exhaustive]
pub enum SessionError {
#[error("sessions not supported: {0}")]
Unsupported(String),
#[error("session not found: {0}")]
NotFound(String),
#[error("stream is not registered for sessions: {0}")]
NotRegistered(String),
#[error("invalid session request: {0}")]
Invalid(String),
#[error("session read not authorized: {0}")]
Unauthorized(String),
#[error("stale session generation: {0}")]
Stale(String),
#[error("session backend error: {0}")]
Backend(String),
#[error("temporarily unavailable: {0}")]
Unavailable(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionHeartbeat {
pub process: String,
pub stream: String,
pub sessions: Vec<ConversationId>,
}
impl crate::validate::Validate for SessionHeartbeat {
fn validate(&self) -> Result<(), crate::error::InvalidError> {
if self.sessions.len() > crate::limits::MAX_HEARTBEAT_SESSIONS {
return Err(crate::error::InvalidError::new(
"heartbeat lists too many sessions",
));
}
Ok(())
}
}
#[expect(clippy::trivially_copy_pass_by_ref, reason = "serde skip predicate")]
fn is_zero_series(series: &u64) -> bool {
*series == 0
}