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    SessionParticipantId, 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/// Kind of actor participating in a session.
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121#[cfg_attr(feature = "openapi", derive(ToSchema))]
122#[serde(rename_all = "snake_case")]
123pub enum SessionParticipantKind {
124    Agent,
125    User,
126}
127
128impl std::fmt::Display for SessionParticipantKind {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            SessionParticipantKind::Agent => write!(f, "agent"),
132            SessionParticipantKind::User => write!(f, "user"),
133        }
134    }
135}
136
137impl From<&str> for SessionParticipantKind {
138    fn from(s: &str) -> Self {
139        match s {
140            "agent" => SessionParticipantKind::Agent,
141            "user" => SessionParticipantKind::User,
142            _ => SessionParticipantKind::User,
143        }
144    }
145}
146
147/// Role a participant has inside a session.
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149#[cfg_attr(feature = "openapi", derive(ToSchema))]
150#[serde(rename_all = "snake_case")]
151pub enum SessionParticipantRole {
152    Host,
153    Member,
154}
155
156impl std::fmt::Display for SessionParticipantRole {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        match self {
159            SessionParticipantRole::Host => write!(f, "host"),
160            SessionParticipantRole::Member => write!(f, "member"),
161        }
162    }
163}
164
165impl From<&str> for SessionParticipantRole {
166    fn from(s: &str) -> Self {
167        match s {
168            "host" => SessionParticipantRole::Host,
169            "member" => SessionParticipantRole::Member,
170            _ => SessionParticipantRole::Member,
171        }
172    }
173}
174
175/// Session participant - an agent or user that has joined a session.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[cfg_attr(feature = "openapi", derive(ToSchema))]
178pub struct SessionParticipant {
179    /// Unique identifier for the participant row (format: part_{32-hex}).
180    #[cfg_attr(
181        feature = "openapi",
182        schema(
183            value_type = String,
184            example = "part_01933b5a00007000800000000000001"
185        )
186    )]
187    pub id: SessionParticipantId,
188    /// Session this participant belongs to.
189    #[cfg_attr(
190        feature = "openapi",
191        schema(
192            value_type = String,
193            example = "session_01933b5a00007000800000000000001"
194        )
195    )]
196    pub session_id: SessionId,
197    pub kind: SessionParticipantKind,
198    /// Present for agent participants.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    #[cfg_attr(
201        feature = "openapi",
202        schema(
203            value_type = Option<String>,
204            example = "agent_01933b5a00007000800000000000001"
205        )
206    )]
207    pub agent_id: Option<AgentId>,
208    /// Immutable agent version captured for an agent participant when known.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[cfg_attr(
211        feature = "openapi",
212        schema(
213            value_type = Option<String>,
214            example = "agentver_01933b5a00007000800000000000001"
215        )
216    )]
217    pub agent_version_id: Option<AgentVersionId>,
218    /// Principal that joined the session.
219    #[cfg_attr(
220        feature = "openapi",
221        schema(
222            value_type = String,
223            example = "principal_01933b5a000070008000000000000001"
224        )
225    )]
226    pub principal_id: PrincipalId,
227    pub role: SessionParticipantRole,
228    pub joined_at: DateTime<Utc>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub left_at: Option<DateTime<Utc>>,
231}
232
233/// Session - instance of agentic loop execution.
234/// A session represents a single conversation with an agent.
235#[derive(Debug, Clone, Serialize, Deserialize)]
236#[cfg_attr(feature = "openapi", derive(ToSchema))]
237pub struct Session {
238    /// Unique identifier for the session (format: session_{32-hex}).
239    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
240    pub id: SessionId,
241    /// Organization this session belongs to (format: org_{32-hex}).
242    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "org_00000000000000000000000000000001"))]
243    pub organization_id: String,
244    /// Workspace this session is attached to (format: wsp_{32-hex}). Owns the
245    /// session's virtual filesystem. For the default 1:1 case this mirrors the
246    /// session id, but clients should read it here rather than deriving it.
247    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "wsp_01933b5a00007000800000000000001"))]
248    pub workspace_id: WorkspaceId,
249    /// ID of the harness for this session (format: harness_{32-hex}).
250    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
251    pub harness_id: HarnessId,
252    /// ID of the agent working in this session (format: agent_{32-hex}). Optional.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
255    pub agent_id: Option<AgentId>,
256    /// Immutable agent version captured when the session was created or rebound.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agentver_01933b5a00007000800000000000001"))]
259    pub agent_version_id: Option<AgentVersionId>,
260    /// Optional resident agent identity for unattended/background execution.
261    #[serde(skip_serializing_if = "Option::is_none")]
262    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "identity_01933b5a00007000800000000000001"))]
263    pub agent_identity_id: Option<AgentIdentityId>,
264    /// Owning principal for this session.
265    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "principal_01933b5a000070008000000000000001"))]
266    pub owner_principal_id: PrincipalId,
267    /// Denormalized effective human owner of the owning principal lineage.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    #[cfg_attr(
270        feature = "openapi",
271        schema(example = "550e8400-e29b-41d4-a716-446655440000")
272    )]
273    pub resolved_owner_user_id: Option<uuid::Uuid>,
274    /// Owning principal summary.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub owner: Option<PrincipalSummary>,
277    /// Effective human owner summary.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub effective_owner: Option<PrincipalSummary>,
280    /// Human-readable title for the session.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    #[cfg_attr(feature = "openapi", schema(example = "Q3 marketing brief"))]
283    pub title: Option<String>,
284    /// Session objective visible to the runtime agent at system-prompt level.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    #[cfg_attr(
287        feature = "openapi",
288        schema(example = "Investigate the queue latency regression")
289    )]
290    pub goal: Option<String>,
291    /// Locale for localized agent behavior and formatting (BCP 47, e.g. `uk-UA`).
292    #[serde(skip_serializing_if = "Option::is_none")]
293    #[cfg_attr(feature = "openapi", schema(example = "en-US"))]
294    pub locale: Option<String>,
295    /// Preview text from the first user message (truncated).
296    #[serde(skip_serializing_if = "Option::is_none")]
297    #[cfg_attr(
298        feature = "openapi",
299        schema(example = "Help me draft the Q3 marketing plan")
300    )]
301    pub preview: Option<String>,
302    /// Preview text from the last assistant response (truncated).
303    #[serde(skip_serializing_if = "Option::is_none")]
304    #[cfg_attr(
305        feature = "openapi",
306        schema(example = "Here is a Q3 plan covering the three pillars we discussed...")
307    )]
308    pub output_preview: Option<String>,
309    /// Tags for organizing and filtering sessions.
310    #[serde(default)]
311    #[cfg_attr(feature = "openapi", schema(example = json!(["marketing", "q3", "draft"])))]
312    pub tags: Vec<String>,
313    /// LLM model ID to use for this session (format: model_{32-hex}).
314    /// Overrides the agent's default model if set.
315    #[serde(skip_serializing_if = "Option::is_none")]
316    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
317    pub model_id: Option<ModelId>,
318    /// Session-level capabilities (additive to agent capabilities).
319    /// Applied after agent capabilities when building RuntimeAgent.
320    #[serde(default, skip_serializing_if = "Vec::is_empty")]
321    pub capabilities: Vec<AgentCapabilityConfig>,
322    /// Client-side tools for this session (additive to agent tools).
323    #[serde(default, skip_serializing_if = "Vec::is_empty")]
324    pub tools: Vec<ToolDefinition>,
325    /// Remote MCP servers scoped to this session only.
326    #[serde(
327        default,
328        rename = "mcpServers",
329        alias = "mcp_servers",
330        skip_serializing_if = "scoped_mcp_servers_is_empty"
331    )]
332    pub mcp_servers: ScopedMcpServers,
333    /// Session-level system prompt override.
334    /// Prepended to the agent's system prompt when building RuntimeAgent.
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub system_prompt: Option<String>,
337    /// Session-level initial files (additive to agent initial_files).
338    /// Files with matching paths override agent/harness files; new paths are appended.
339    #[serde(default, skip_serializing_if = "Vec::is_empty")]
340    pub initial_files: Vec<crate::session_file::InitialFile>,
341    /// Session-level client hints — arbitrary key-value pairs declared by the
342    /// client at session creation time. These are defaults for every turn;
343    /// per-message `controls.hints` override these key-by-key (shallow merge).
344    ///
345    /// Examples: `{"setup_connection": true, "rich_media": true}`
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
348    /// Network access list controlling which hosts/URLs this session can reach.
349    /// Merged with harness and agent layers (allowed: intersect, blocked: union).
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub network_access: Option<NetworkAccessList>,
352    /// Maximum number of LLM iterations per turn for this session.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    #[cfg_attr(feature = "openapi", schema(example = 50))]
355    pub max_iterations: Option<usize>,
356    /// Request-level parallel tool calling preference (EVE-598).
357    ///
358    /// `None` (default) preserves provider defaults. `Some(true)` signals the
359    /// provider that parallel tool calls are wanted; `Some(false)` requests at
360    /// most one tool call per turn and forces serial execution. Merged across
361    /// harness/agent/session layers (overlay wins).
362    #[serde(default, skip_serializing_if = "Option::is_none")]
363    #[cfg_attr(feature = "openapi", schema(example = true))]
364    pub parallel_tool_calls: Option<bool>,
365    /// Current execution status of the session.
366    pub status: SessionStatus,
367    /// Timestamp when the session was created.
368    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:00Z"))]
369    pub created_at: DateTime<Utc>,
370    /// Timestamp when the session was last updated.
371    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
372    pub updated_at: DateTime<Utc>,
373    /// Timestamp when the session started executing.
374    #[serde(skip_serializing_if = "Option::is_none")]
375    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:01Z"))]
376    pub started_at: Option<DateTime<Utc>>,
377    /// Timestamp when the session finished (completed or failed).
378    #[serde(skip_serializing_if = "Option::is_none")]
379    #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
380    pub finished_at: Option<DateTime<Utc>>,
381    /// Cumulative token usage for all LLM calls in this session.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub usage: Option<TokenUsage>,
384    /// Whether this session is pinned by the current user.
385    /// Only populated when the request has an authenticated user context.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    #[cfg_attr(feature = "openapi", schema(example = false))]
388    pub is_pinned: Option<bool>,
389    /// Number of active (enabled) schedules for this session.
390    /// Populated when the session is fetched for API responses.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    #[cfg_attr(feature = "openapi", schema(example = 2))]
393    pub active_schedule_count: Option<u32>,
394    /// Aggregated UI features from all active capabilities (harness + agent + session).
395    /// Computed at read time from the capability registry.
396    /// Known features: "file_system", "schedules", "secrets", "key_value",
397    /// "sql_database", "leased_resources".
398    #[serde(default, skip_serializing_if = "Vec::is_empty")]
399    #[cfg_attr(feature = "openapi", schema(example = json!(["file_system", "secrets"])))]
400    pub features: Vec<String>,
401
402    // -- Subagent nesting fields --
403    /// Parent session that spawned this subagent. NULL for top-level sessions.
404    /// Used to compute governed subagent delegation depth.
405    #[serde(skip_serializing_if = "Option::is_none")]
406    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
407    pub parent_session_id: Option<SessionId>,
408
409    // -- Fork lineage fields (specs/forking-sessions.md) --
410    /// Session this one was forked from. NULL for sessions that were not forked.
411    /// Distinct from `parent_session_id` (subagent nesting): forking is a
412    /// user-initiated "branch from here" relationship.
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
415    pub forked_from_session_id: Option<SessionId>,
416    /// Parent event sequence the fork was taken at (the fork point). NULL unless
417    /// this session is a fork.
418    #[serde(default, skip_serializing_if = "Option::is_none")]
419    #[cfg_attr(feature = "openapi", schema(example = 42))]
420    pub forked_from_sequence: Option<i32>,
421
422    // -- Blueprint fields (only set when this session runs a blueprint agent) --
423    /// Blueprint ID. When set, reason_activity and act_activity build RuntimeAgent
424    /// from the blueprint definition instead of from harness_id/agent_id.
425    #[serde(skip_serializing_if = "Option::is_none")]
426    #[cfg_attr(feature = "openapi", schema(example = "blueprint_research_pack"))]
427    pub blueprint_id: Option<String>,
428    /// Validated config passed by host at blueprint spawn time.
429    /// Example: `{"target_repo": "acme/everruns"}`.
430    #[serde(skip_serializing_if = "Option::is_none")]
431    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
432    pub blueprint_config: Option<serde_json::Value>,
433}
434
435/// Seed mode used when creating a peer session from an existing session.
436#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
437#[cfg_attr(feature = "openapi", derive(ToSchema))]
438#[serde(rename_all = "snake_case")]
439pub enum SessionSeedMode {
440    /// Create an empty session and only record lineage when provided.
441    #[default]
442    Fresh,
443    /// Copy conversation events, workspace files, and durable session storage.
444    Fork,
445    /// Copy workspace files only.
446    Workspace,
447}
448
449impl SessionSeedMode {
450    pub fn as_str(self) -> &'static str {
451        match self {
452            Self::Fresh => "fresh",
453            Self::Fork => "fork",
454            Self::Workspace => "workspace",
455        }
456    }
457}