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