Skip to main content

everruns_core/
session.rs

1// Session domain types
2//
3// These types represent the Session entity and its status.
4// Used by both API and worker crates.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::capability_types::AgentCapabilityConfig;
10use crate::events::TokenUsage;
11use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
12use crate::network_access::NetworkAccessList;
13use crate::principal::PrincipalSummary;
14use crate::tool_types::ToolDefinition;
15use crate::typed_id::{
16    AgentId, AgentIdentityId, AgentVersionId, HarnessId, ModelId, PrincipalId, SessionId,
17};
18
19#[cfg(feature = "openapi")]
20use utoipa::ToSchema;
21
22/// Subagent lifecycle status.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[cfg_attr(feature = "openapi", derive(ToSchema))]
25#[serde(rename_all = "snake_case")]
26pub enum SubagentStatus {
27    Spawning,
28    Running,
29    Completed,
30    Failed,
31    Cancelled,
32    MaxIterationsReached,
33}
34
35impl std::fmt::Display for SubagentStatus {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            SubagentStatus::Spawning => write!(f, "spawning"),
39            SubagentStatus::Running => write!(f, "running"),
40            SubagentStatus::Completed => write!(f, "completed"),
41            SubagentStatus::Failed => write!(f, "failed"),
42            SubagentStatus::Cancelled => write!(f, "cancelled"),
43            SubagentStatus::MaxIterationsReached => write!(f, "max_iterations_reached"),
44        }
45    }
46}
47
48impl From<&str> for SubagentStatus {
49    fn from(s: &str) -> Self {
50        match s {
51            "spawning" => SubagentStatus::Spawning,
52            "running" => SubagentStatus::Running,
53            "completed" => SubagentStatus::Completed,
54            "failed" => SubagentStatus::Failed,
55            "cancelled" => SubagentStatus::Cancelled,
56            "max_iterations_reached" => SubagentStatus::MaxIterationsReached,
57            _ => SubagentStatus::Spawning,
58        }
59    }
60}
61
62/// Session execution status.
63/// - `started`: Session just created, no turn executed yet
64/// - `active`: A turn is currently running
65/// - `idle`: Turn completed, session waiting for next input
66/// - `paused`: Budget limit reached, waiting for user to increase limit or resume
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68#[cfg_attr(feature = "openapi", derive(ToSchema))]
69#[serde(rename_all = "lowercase")]
70pub enum SessionStatus {
71    /// Session just created, no turn executed yet.
72    Started,
73    /// A turn is currently running (session is active).
74    Active,
75    /// Turn completed, session waiting for next input (idle).
76    Idle,
77    /// Waiting for client to submit tool results.
78    WaitingForToolResults,
79    /// Budget limit reached — session paused until user resumes or increases limit.
80    Paused,
81}
82
83impl std::fmt::Display for SessionStatus {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            SessionStatus::Started => write!(f, "started"),
87            SessionStatus::Active => write!(f, "active"),
88            SessionStatus::Idle => write!(f, "idle"),
89            SessionStatus::WaitingForToolResults => write!(f, "waiting_for_tool_results"),
90            SessionStatus::Paused => write!(f, "paused"),
91        }
92    }
93}
94
95impl From<&str> for SessionStatus {
96    fn from(s: &str) -> Self {
97        match s {
98            "active" => SessionStatus::Active,
99            "idle" => SessionStatus::Idle,
100            "waiting_for_tool_results" => SessionStatus::WaitingForToolResults,
101            "paused" => SessionStatus::Paused,
102            // Handle legacy values during migration
103            "running" => SessionStatus::Active,
104            "pending" | "completed" | "failed" => SessionStatus::Idle,
105            _ => SessionStatus::Started,
106        }
107    }
108}
109
110/// Session - instance of agentic loop execution.
111/// A session represents a single conversation with an agent.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[cfg_attr(feature = "openapi", derive(ToSchema))]
114pub struct Session {
115    /// Unique identifier for the session (format: session_{32-hex}).
116    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
117    pub id: SessionId,
118    /// Organization this session belongs to (format: org_{32-hex}).
119    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "org_00000000000000000000000000000001"))]
120    pub organization_id: String,
121    /// ID of the harness for this session (format: harness_{32-hex}).
122    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
123    pub harness_id: HarnessId,
124    /// ID of the agent working in this session (format: agent_{32-hex}). Optional.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
127    pub agent_id: Option<AgentId>,
128    /// Immutable agent version captured when the session was created or rebound.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agentver_01933b5a00007000800000000000001"))]
131    pub agent_version_id: Option<AgentVersionId>,
132    /// Optional resident agent identity for unattended/background execution.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "identity_01933b5a00007000800000000000001"))]
135    pub agent_identity_id: Option<AgentIdentityId>,
136    /// Owning principal for this session.
137    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "principal_01933b5a000070008000000000000001"))]
138    pub owner_principal_id: PrincipalId,
139    /// Denormalized effective human owner of the owning principal lineage.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    #[cfg_attr(
142        feature = "openapi",
143        schema(example = "550e8400-e29b-41d4-a716-446655440000")
144    )]
145    pub resolved_owner_user_id: Option<uuid::Uuid>,
146    /// Owning principal summary.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub owner: Option<PrincipalSummary>,
149    /// Effective human owner summary.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub effective_owner: Option<PrincipalSummary>,
152    /// Human-readable title for the session.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    #[cfg_attr(feature = "openapi", schema(example = "Q3 marketing brief"))]
155    pub title: Option<String>,
156    /// Locale for localized agent behavior and formatting (BCP 47, e.g. `uk-UA`).
157    #[serde(skip_serializing_if = "Option::is_none")]
158    #[cfg_attr(feature = "openapi", schema(example = "en-US"))]
159    pub locale: Option<String>,
160    /// Preview text from the first user message (truncated).
161    #[serde(skip_serializing_if = "Option::is_none")]
162    #[cfg_attr(
163        feature = "openapi",
164        schema(example = "Help me draft the Q3 marketing plan")
165    )]
166    pub preview: Option<String>,
167    /// Preview text from the last assistant response (truncated).
168    #[serde(skip_serializing_if = "Option::is_none")]
169    #[cfg_attr(
170        feature = "openapi",
171        schema(example = "Here is a Q3 plan covering the three pillars we discussed...")
172    )]
173    pub output_preview: Option<String>,
174    /// Tags for organizing and filtering sessions.
175    #[serde(default)]
176    #[cfg_attr(feature = "openapi", schema(example = json!(["marketing", "q3", "draft"])))]
177    pub tags: Vec<String>,
178    /// LLM model ID to use for this session (format: model_{32-hex}).
179    /// Overrides the agent's default model if set.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
182    pub model_id: Option<ModelId>,
183    /// Session-level capabilities (additive to agent capabilities).
184    /// Applied after agent capabilities when building RuntimeAgent.
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    pub capabilities: Vec<AgentCapabilityConfig>,
187    /// Client-side tools for this session (additive to agent tools).
188    #[serde(default, skip_serializing_if = "Vec::is_empty")]
189    pub tools: Vec<ToolDefinition>,
190    /// Remote MCP servers scoped to this session only.
191    #[serde(
192        default,
193        rename = "mcpServers",
194        alias = "mcp_servers",
195        skip_serializing_if = "scoped_mcp_servers_is_empty"
196    )]
197    pub mcp_servers: ScopedMcpServers,
198    /// Session-level system prompt override.
199    /// Prepended to the agent's system prompt when building RuntimeAgent.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub system_prompt: Option<String>,
202    /// Session-level initial files (additive to agent initial_files).
203    /// Files with matching paths override agent/harness files; new paths are appended.
204    #[serde(default, skip_serializing_if = "Vec::is_empty")]
205    pub initial_files: Vec<crate::session_file::InitialFile>,
206    /// Session-level client hints — arbitrary key-value pairs declared by the
207    /// client at session creation time. These are defaults for every turn;
208    /// per-message `controls.hints` override these key-by-key (shallow merge).
209    ///
210    /// Examples: `{"setup_connection": true, "rich_media": true}`
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
213    /// Network access list controlling which hosts/URLs this session can reach.
214    /// Merged with harness and agent layers (allowed: intersect, blocked: union).
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub network_access: Option<NetworkAccessList>,
217    /// Maximum number of LLM iterations per turn for this session.
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    #[cfg_attr(feature = "openapi", schema(example = 50))]
220    pub max_iterations: Option<usize>,
221    /// Current execution status of the session.
222    pub status: SessionStatus,
223    /// Timestamp when the session was created.
224    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:00Z"))]
225    pub created_at: DateTime<Utc>,
226    /// Timestamp when the session was last updated.
227    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
228    pub updated_at: DateTime<Utc>,
229    /// Timestamp when the session started executing.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:01Z"))]
232    pub started_at: Option<DateTime<Utc>>,
233    /// Timestamp when the session finished (completed or failed).
234    #[serde(skip_serializing_if = "Option::is_none")]
235    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
236    pub finished_at: Option<DateTime<Utc>>,
237    /// Cumulative token usage for all LLM calls in this session.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub usage: Option<TokenUsage>,
240    /// Whether this session is pinned by the current user.
241    /// Only populated when the request has an authenticated user context.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    #[cfg_attr(feature = "openapi", schema(example = false))]
244    pub is_pinned: Option<bool>,
245    /// Number of active (enabled) schedules for this session.
246    /// Populated when the session is fetched for API responses.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    #[cfg_attr(feature = "openapi", schema(example = 2))]
249    pub active_schedule_count: Option<u32>,
250    /// Aggregated UI features from all active capabilities (harness + agent + session).
251    /// Computed at read time from the capability registry.
252    /// Known features: "file_system", "schedules", "secrets", "key_value",
253    /// "sql_database", "leased_resources".
254    #[serde(default, skip_serializing_if = "Vec::is_empty")]
255    #[cfg_attr(feature = "openapi", schema(example = json!(["file_system", "secrets"])))]
256    pub features: Vec<String>,
257
258    // -- Subagent fields (only set when this session is a subagent) --
259    /// Parent session that spawned this subagent. NULL for top-level sessions.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
262    pub parent_session_id: Option<SessionId>,
263    /// Human-readable subagent name ("Test Runner"), unique per parent.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    #[cfg_attr(feature = "openapi", schema(example = "Test Runner"))]
266    pub subagent_name: Option<String>,
267    /// Original task description given to this subagent.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    #[cfg_attr(
270        feature = "openapi",
271        schema(example = "Run the integration test suite and report failures.")
272    )]
273    pub subagent_task: Option<String>,
274    /// Subagent lifecycle status: spawning, running, completed, failed, cancelled.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub subagent_status: Option<SubagentStatus>,
277
278    // -- Blueprint fields (only set when this session runs a blueprint agent) --
279    /// Blueprint ID. When set, reason_activity and act_activity build RuntimeAgent
280    /// from the blueprint definition instead of from harness_id/agent_id.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    #[cfg_attr(feature = "openapi", schema(example = "blueprint_research_pack"))]
283    pub blueprint_id: Option<String>,
284    /// Validated config passed by host at blueprint spawn time.
285    /// Example: `{"target_repo": "acme/everruns"}`.
286    #[serde(skip_serializing_if = "Option::is_none")]
287    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
288    pub blueprint_config: Option<serde_json::Value>,
289}