everruns-core 0.9.0

Core agent abstractions for Everruns - agent loop, events, tools, LLM providers
Documentation
// Session domain types
//
// These types represent the Session entity and its status.
// Used by both API and worker crates.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::capability_types::AgentCapabilityConfig;
use crate::events::TokenUsage;
use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
use crate::network_access::NetworkAccessList;
use crate::principal::PrincipalSummary;
use crate::tool_types::ToolDefinition;
use crate::typed_id::{
    AgentId, AgentIdentityId, AgentVersionId, HarnessId, ModelId, PrincipalId, SessionId,
};

#[cfg(feature = "openapi")]
use utoipa::ToSchema;

/// Subagent lifecycle status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum SubagentStatus {
    Spawning,
    Running,
    Completed,
    Failed,
    Cancelled,
    MaxIterationsReached,
}

impl std::fmt::Display for SubagentStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SubagentStatus::Spawning => write!(f, "spawning"),
            SubagentStatus::Running => write!(f, "running"),
            SubagentStatus::Completed => write!(f, "completed"),
            SubagentStatus::Failed => write!(f, "failed"),
            SubagentStatus::Cancelled => write!(f, "cancelled"),
            SubagentStatus::MaxIterationsReached => write!(f, "max_iterations_reached"),
        }
    }
}

impl From<&str> for SubagentStatus {
    fn from(s: &str) -> Self {
        match s {
            "spawning" => SubagentStatus::Spawning,
            "running" => SubagentStatus::Running,
            "completed" => SubagentStatus::Completed,
            "failed" => SubagentStatus::Failed,
            "cancelled" => SubagentStatus::Cancelled,
            "max_iterations_reached" => SubagentStatus::MaxIterationsReached,
            _ => SubagentStatus::Spawning,
        }
    }
}

/// Session execution status.
/// - `started`: Session just created, no turn executed yet
/// - `active`: A turn is currently running
/// - `idle`: Turn completed, session waiting for next input
/// - `paused`: Budget limit reached, waiting for user to increase limit or resume
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum SessionStatus {
    /// Session just created, no turn executed yet.
    Started,
    /// A turn is currently running (session is active).
    Active,
    /// Turn completed, session waiting for next input (idle).
    Idle,
    /// Waiting for client to submit tool results.
    WaitingForToolResults,
    /// Budget limit reached — session paused until user resumes or increases limit.
    Paused,
}

impl std::fmt::Display for SessionStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SessionStatus::Started => write!(f, "started"),
            SessionStatus::Active => write!(f, "active"),
            SessionStatus::Idle => write!(f, "idle"),
            SessionStatus::WaitingForToolResults => write!(f, "waiting_for_tool_results"),
            SessionStatus::Paused => write!(f, "paused"),
        }
    }
}

impl From<&str> for SessionStatus {
    fn from(s: &str) -> Self {
        match s {
            "active" => SessionStatus::Active,
            "idle" => SessionStatus::Idle,
            "waiting_for_tool_results" => SessionStatus::WaitingForToolResults,
            "paused" => SessionStatus::Paused,
            // Handle legacy values during migration
            "running" => SessionStatus::Active,
            "pending" | "completed" | "failed" => SessionStatus::Idle,
            _ => SessionStatus::Started,
        }
    }
}

/// Session - instance of agentic loop execution.
/// A session represents a single conversation with an agent.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct Session {
    /// Unique identifier for the session (format: session_{32-hex}).
    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
    pub id: SessionId,
    /// Organization this session belongs to (format: org_{32-hex}).
    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "org_00000000000000000000000000000001"))]
    pub organization_id: String,
    /// ID of the harness for this session (format: harness_{32-hex}).
    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
    pub harness_id: HarnessId,
    /// ID of the agent working in this session (format: agent_{32-hex}). Optional.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
    pub agent_id: Option<AgentId>,
    /// Immutable agent version captured when the session was created or rebound.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agentver_01933b5a00007000800000000000001"))]
    pub agent_version_id: Option<AgentVersionId>,
    /// Optional resident agent identity for unattended/background execution.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "identity_01933b5a00007000800000000000001"))]
    pub agent_identity_id: Option<AgentIdentityId>,
    /// Owning principal for this session.
    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "principal_01933b5a000070008000000000000001"))]
    pub owner_principal_id: PrincipalId,
    /// Denormalized effective human owner of the owning principal lineage.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(
        feature = "openapi",
        schema(example = "550e8400-e29b-41d4-a716-446655440000")
    )]
    pub resolved_owner_user_id: Option<uuid::Uuid>,
    /// Owning principal summary.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub owner: Option<PrincipalSummary>,
    /// Effective human owner summary.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub effective_owner: Option<PrincipalSummary>,
    /// Human-readable title for the session.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "Q3 marketing brief"))]
    pub title: Option<String>,
    /// Locale for localized agent behavior and formatting (BCP 47, e.g. `uk-UA`).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "en-US"))]
    pub locale: Option<String>,
    /// Preview text from the first user message (truncated).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(
        feature = "openapi",
        schema(example = "Help me draft the Q3 marketing plan")
    )]
    pub preview: Option<String>,
    /// Preview text from the last assistant response (truncated).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(
        feature = "openapi",
        schema(example = "Here is a Q3 plan covering the three pillars we discussed...")
    )]
    pub output_preview: Option<String>,
    /// Tags for organizing and filtering sessions.
    #[serde(default)]
    #[cfg_attr(feature = "openapi", schema(example = json!(["marketing", "q3", "draft"])))]
    pub tags: Vec<String>,
    /// LLM model ID to use for this session (format: model_{32-hex}).
    /// Overrides the agent's default model if set.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
    pub model_id: Option<ModelId>,
    /// Session-level capabilities (additive to agent capabilities).
    /// Applied after agent capabilities when building RuntimeAgent.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub capabilities: Vec<AgentCapabilityConfig>,
    /// Client-side tools for this session (additive to agent tools).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tools: Vec<ToolDefinition>,
    /// Remote MCP servers scoped to this session only.
    #[serde(
        default,
        rename = "mcpServers",
        alias = "mcp_servers",
        skip_serializing_if = "scoped_mcp_servers_is_empty"
    )]
    pub mcp_servers: ScopedMcpServers,
    /// Session-level system prompt override.
    /// Prepended to the agent's system prompt when building RuntimeAgent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub system_prompt: Option<String>,
    /// Session-level initial files (additive to agent initial_files).
    /// Files with matching paths override agent/harness files; new paths are appended.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub initial_files: Vec<crate::session_file::InitialFile>,
    /// Session-level client hints — arbitrary key-value pairs declared by the
    /// client at session creation time. These are defaults for every turn;
    /// per-message `controls.hints` override these key-by-key (shallow merge).
    ///
    /// Examples: `{"setup_connection": true, "rich_media": true}`
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
    /// Network access list controlling which hosts/URLs this session can reach.
    /// Merged with harness and agent layers (allowed: intersect, blocked: union).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub network_access: Option<NetworkAccessList>,
    /// Maximum number of LLM iterations per turn for this session.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = 50))]
    pub max_iterations: Option<usize>,
    /// Current execution status of the session.
    pub status: SessionStatus,
    /// Timestamp when the session was created.
    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:00Z"))]
    pub created_at: DateTime<Utc>,
    /// Timestamp when the session was last updated.
    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
    pub updated_at: DateTime<Utc>,
    /// Timestamp when the session started executing.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:01Z"))]
    pub started_at: Option<DateTime<Utc>>,
    /// Timestamp when the session finished (completed or failed).
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
    pub finished_at: Option<DateTime<Utc>>,
    /// Cumulative token usage for all LLM calls in this session.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub usage: Option<TokenUsage>,
    /// Whether this session is pinned by the current user.
    /// Only populated when the request has an authenticated user context.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = false))]
    pub is_pinned: Option<bool>,
    /// Number of active (enabled) schedules for this session.
    /// Populated when the session is fetched for API responses.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = 2))]
    pub active_schedule_count: Option<u32>,
    /// Aggregated UI features from all active capabilities (harness + agent + session).
    /// Computed at read time from the capability registry.
    /// Known features: "file_system", "schedules", "secrets", "key_value",
    /// "sql_database", "leased_resources".
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[cfg_attr(feature = "openapi", schema(example = json!(["file_system", "secrets"])))]
    pub features: Vec<String>,

    // -- Subagent fields (only set when this session is a subagent) --
    /// Parent session that spawned this subagent. NULL for top-level sessions.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
    pub parent_session_id: Option<SessionId>,
    /// Human-readable subagent name ("Test Runner"), unique per parent.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "Test Runner"))]
    pub subagent_name: Option<String>,
    /// Original task description given to this subagent.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(
        feature = "openapi",
        schema(example = "Run the integration test suite and report failures.")
    )]
    pub subagent_task: Option<String>,
    /// Subagent lifecycle status: spawning, running, completed, failed, cancelled.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subagent_status: Option<SubagentStatus>,

    // -- Blueprint fields (only set when this session runs a blueprint agent) --
    /// Blueprint ID. When set, reason_activity and act_activity build RuntimeAgent
    /// from the blueprint definition instead of from harness_id/agent_id.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(example = "blueprint_research_pack"))]
    pub blueprint_id: Option<String>,
    /// Validated config passed by host at blueprint spawn time.
    /// Example: `{"target_repo": "acme/everruns"}`.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
    pub blueprint_config: Option<serde_json::Value>,
}