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