1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// Neutral session identity and turn execution state (EVE-882).
//
// Decision: the persisted `Session` database/API aggregate — source/facet
// classifications, participants and ownership references, UI/list activity
// projections, timestamps, catalog relationships — lives in
// `everruns-platform`. Core keeps only this portable, execution-facing view:
// the session correlation values and effective per-session configuration a
// turn consumes, plus the small neutral execution state the host lifecycle
// drives. The platform loading seam (server repositories, worker adapters,
// hosted stores) projects stored records into these values before host
// execution begins — the host never requests or receives a stored Session.
use std::collections::HashMap;
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::session_file::InitialFile;
use crate::tool_types::ToolDefinition;
use crate::typed_id::{AgentId, HarnessId, ModelId, SessionId, WorkspaceId};
/// Subagent lifecycle status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum SubagentStatus {
Spawning,
Running,
Completed,
Failed,
Cancelled,
MaxIterationsReached,
/// The durable engine deliberately stopped (sealed) the child's turn to
/// prevent further waste (no forward progress, or budget exhausted). This is
/// terminal and non-retryable, and is intentionally distinct from `Failed`
/// so the parent agent can decide what to do next (the seal reason is
/// carried in the child's final assistant message / spawn `result`).
Sealed,
}
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"),
SubagentStatus::Sealed => write!(f, "sealed"),
}
}
}
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,
"sealed" => SubagentStatus::Sealed,
_ => SubagentStatus::Spawning,
}
}
}
/// Neutral session execution state (EVE-882).
///
/// The small value host planning and lifecycle transitions operate on:
/// - `started`: session created, no turn executed yet
/// - `active`: a turn is currently running
/// - `idle`: turn completed, session waiting for next input
/// - `waiting_for_tool_results`: waiting for the client to submit tool results
/// - `paused`: budget limit reached, waiting for the user to resume
///
/// The persisted product status enum lives in `everruns-platform`
/// (`SessionStatus`) and maps to/from this value at the adapter boundary; its
/// wire strings are the [`SessionExecutionState::as_str`] values below.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SessionExecutionState {
/// 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 SessionExecutionState {
pub fn as_str(self) -> &'static str {
match self {
SessionExecutionState::Started => "started",
SessionExecutionState::Active => "active",
SessionExecutionState::Idle => "idle",
SessionExecutionState::WaitingForToolResults => "waiting_for_tool_results",
SessionExecutionState::Paused => "paused",
}
}
}
impl std::fmt::Display for SessionExecutionState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<&str> for SessionExecutionState {
fn from(s: &str) -> Self {
match s {
"active" => SessionExecutionState::Active,
"idle" => SessionExecutionState::Idle,
"waiting_for_tool_results" => SessionExecutionState::WaitingForToolResults,
"paused" => SessionExecutionState::Paused,
// Handle legacy values during migration
"running" => SessionExecutionState::Active,
"pending" | "completed" | "failed" => SessionExecutionState::Idle,
_ => SessionExecutionState::Started,
}
}
}
/// Portable execution view of one session (EVE-882).
///
/// Carries exactly what turn execution consumes: the typed correlation
/// values, the session's own configuration overlay layer (leaf of the
/// harness → agent → session chain), the neutral execution state, and
/// cumulative usage accounting. It is not a persistence record — origin
/// facets, activity projections, participants, ownership summaries,
/// timestamps, and list/UI metadata stay in `everruns-platform`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionSession {
/// Session identity (correlation value).
pub id: SessionId,
/// Public (`org_…`) organization id used for execution scoping. This is a
/// correlation value, not the organization record.
pub organization_id: String,
/// Workspace owning the session's virtual filesystem. For the default 1:1
/// case this mirrors the session id.
pub workspace_id: WorkspaceId,
/// Harness providing the base environment configuration layer.
pub harness_id: HarnessId,
/// Optional agent working in this session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<AgentId>,
/// Human-readable title (readable/writable through the session capability).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Session objective visible to the runtime agent at system-prompt level.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
/// Locale for localized agent behavior and formatting (BCP 47).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
/// Tags consulted by execution modes (e.g. progress reporting).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
/// Session-level model override (higher priority than agent/harness).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_id: Option<ModelId>,
/// Session-level capabilities (additive to agent capabilities).
#[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.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
/// Session-level initial files (additive to agent initial_files).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub initial_files: Vec<InitialFile>,
/// Session-level client hints; per-message `controls.hints` override these
/// key-by-key (shallow merge).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hints: Option<HashMap<String, serde_json::Value>>,
/// Network access list merged with harness and agent layers.
#[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")]
pub max_iterations: Option<usize>,
/// Request-level parallel tool calling preference (EVE-598).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
/// Neutral execution state driven by the host lifecycle.
pub status: SessionExecutionState,
/// Cumulative token usage for all LLM calls in this session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
/// Parent session that spawned this subagent (subagent nesting depth).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_session_id: Option<SessionId>,
/// Session this one was forked from (delegation-result correlation).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forked_from_session_id: Option<SessionId>,
/// Blueprint ID. When set, execution builds the RuntimeAgent from the
/// blueprint definition instead of harness/agent configuration.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blueprint_id: Option<String>,
/// Validated config passed by host at blueprint spawn time.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blueprint_config: Option<serde_json::Value>,
}
impl ExecutionSession {
/// Create an execution session with the given correlation identity; all
/// configuration starts empty, status starts at `started`, and the
/// organization defaults to the single-tenant default org.
pub fn new(id: SessionId, workspace_id: WorkspaceId, harness_id: HarnessId) -> Self {
Self {
id,
organization_id: crate::DEFAULT_ORG_PUBLIC_ID.to_string(),
workspace_id,
harness_id,
agent_id: None,
title: None,
goal: None,
locale: None,
tags: Vec::new(),
model_id: None,
capabilities: Vec::new(),
tools: Vec::new(),
mcp_servers: ScopedMcpServers::default(),
system_prompt: None,
initial_files: Vec::new(),
hints: None,
network_access: None,
max_iterations: None,
parallel_tool_calls: None,
status: SessionExecutionState::Started,
usage: None,
parent_session_id: None,
forked_from_session_id: None,
blueprint_id: None,
blueprint_config: None,
}
}
/// Create an execution session under the default 1:1 workspace identity
/// (`workspace.id == session.id`).
pub fn with_own_workspace(id: SessionId, harness_id: HarnessId) -> Self {
Self::new(id, WorkspaceId::from_uuid(id.uuid()), harness_id)
}
}
/// Seed mode used when creating a peer session from an existing session.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum SessionSeedMode {
/// Create an empty session and only record lineage when provided.
#[default]
Fresh,
/// Copy conversation events, workspace files, and durable session storage.
Fork,
/// Copy workspace files only.
Workspace,
}
impl SessionSeedMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Fresh => "fresh",
Self::Fork => "fork",
Self::Workspace => "workspace",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_state_preserves_wire_values_and_legacy_inputs() {
for (state, wire) in [
(SessionExecutionState::Started, "started"),
(SessionExecutionState::Active, "active"),
(SessionExecutionState::Idle, "idle"),
(
SessionExecutionState::WaitingForToolResults,
"waiting_for_tool_results",
),
(SessionExecutionState::Paused, "paused"),
] {
assert_eq!(state.as_str(), wire);
assert_eq!(state.to_string(), wire);
assert_eq!(SessionExecutionState::from(wire), state);
assert_eq!(
serde_json::to_value(state).unwrap(),
serde_json::json!(wire)
);
assert_eq!(
serde_json::from_value::<SessionExecutionState>(serde_json::json!(wire)).unwrap(),
state
);
}
for (legacy, expected) in [
("running", SessionExecutionState::Active),
("pending", SessionExecutionState::Idle),
("completed", SessionExecutionState::Idle),
("failed", SessionExecutionState::Idle),
("garbage", SessionExecutionState::Started),
] {
assert_eq!(SessionExecutionState::from(legacy), expected, "{legacy}");
}
}
#[test]
fn execution_session_preserves_minimal_portable_shape() {
let session = ExecutionSession::with_own_workspace(
"session_01933b5a000070008000000000000001".parse().unwrap(),
"harness_01933b5a000070008000000000000002".parse().unwrap(),
);
assert_eq!(
serde_json::to_value(&session).unwrap(),
serde_json::json!({
"id": "session_01933b5a000070008000000000000001",
"organization_id": "org_00000000000000000000000000000001",
"workspace_id": "wsp_01933b5a000070008000000000000001",
"harness_id": "harness_01933b5a000070008000000000000002",
"status": "started",
})
);
}
}