1use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17use crate::capability_types::AgentCapabilityConfig;
18use crate::events::TokenUsage;
19use crate::mcp_server::{ScopedMcpServers, scoped_mcp_servers_is_empty};
20use crate::network_access::NetworkAccessList;
21use crate::session_file::InitialFile;
22use crate::tool_types::ToolDefinition;
23use crate::typed_id::{AgentId, HarnessId, ModelId, SessionId, WorkspaceId};
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
28#[serde(rename_all = "snake_case")]
29pub enum SubagentStatus {
30 Spawning,
31 Running,
32 Completed,
33 Failed,
34 Cancelled,
35 MaxIterationsReached,
36 Sealed,
42}
43
44impl std::fmt::Display for SubagentStatus {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 SubagentStatus::Spawning => write!(f, "spawning"),
48 SubagentStatus::Running => write!(f, "running"),
49 SubagentStatus::Completed => write!(f, "completed"),
50 SubagentStatus::Failed => write!(f, "failed"),
51 SubagentStatus::Cancelled => write!(f, "cancelled"),
52 SubagentStatus::MaxIterationsReached => write!(f, "max_iterations_reached"),
53 SubagentStatus::Sealed => write!(f, "sealed"),
54 }
55 }
56}
57
58impl From<&str> for SubagentStatus {
59 fn from(s: &str) -> Self {
60 match s {
61 "spawning" => SubagentStatus::Spawning,
62 "running" => SubagentStatus::Running,
63 "completed" => SubagentStatus::Completed,
64 "failed" => SubagentStatus::Failed,
65 "cancelled" => SubagentStatus::Cancelled,
66 "max_iterations_reached" => SubagentStatus::MaxIterationsReached,
67 "sealed" => SubagentStatus::Sealed,
68 _ => SubagentStatus::Spawning,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "snake_case")]
87pub enum SessionExecutionState {
88 Started,
90 Active,
92 Idle,
94 WaitingForToolResults,
96 Paused,
98}
99
100impl SessionExecutionState {
101 pub fn as_str(self) -> &'static str {
102 match self {
103 SessionExecutionState::Started => "started",
104 SessionExecutionState::Active => "active",
105 SessionExecutionState::Idle => "idle",
106 SessionExecutionState::WaitingForToolResults => "waiting_for_tool_results",
107 SessionExecutionState::Paused => "paused",
108 }
109 }
110}
111
112impl std::fmt::Display for SessionExecutionState {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str(self.as_str())
115 }
116}
117
118impl From<&str> for SessionExecutionState {
119 fn from(s: &str) -> Self {
120 match s {
121 "active" => SessionExecutionState::Active,
122 "idle" => SessionExecutionState::Idle,
123 "waiting_for_tool_results" => SessionExecutionState::WaitingForToolResults,
124 "paused" => SessionExecutionState::Paused,
125 "running" => SessionExecutionState::Active,
127 "pending" | "completed" | "failed" => SessionExecutionState::Idle,
128 _ => SessionExecutionState::Started,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ExecutionSession {
143 pub id: SessionId,
145 pub organization_id: String,
148 pub workspace_id: WorkspaceId,
151 pub harness_id: HarnessId,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub agent_id: Option<AgentId>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub title: Option<String>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub goal: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub locale: Option<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub tags: Vec<String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub model_id: Option<ModelId>,
171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
173 pub capabilities: Vec<AgentCapabilityConfig>,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub tools: Vec<ToolDefinition>,
177 #[serde(
179 default,
180 rename = "mcpServers",
181 alias = "mcp_servers",
182 skip_serializing_if = "scoped_mcp_servers_is_empty"
183 )]
184 pub mcp_servers: ScopedMcpServers,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub system_prompt: Option<String>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub initial_files: Vec<InitialFile>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub hints: Option<HashMap<String, serde_json::Value>>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub network_access: Option<NetworkAccessList>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub max_iterations: Option<usize>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub parallel_tool_calls: Option<bool>,
204 pub status: SessionExecutionState,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub usage: Option<TokenUsage>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub parent_session_id: Option<SessionId>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub forked_from_session_id: Option<SessionId>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub blueprint_id: Option<String>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub blueprint_config: Option<serde_json::Value>,
222}
223
224impl ExecutionSession {
225 pub fn new(id: SessionId, workspace_id: WorkspaceId, harness_id: HarnessId) -> Self {
229 Self {
230 id,
231 organization_id: crate::DEFAULT_ORG_PUBLIC_ID.to_string(),
232 workspace_id,
233 harness_id,
234 agent_id: None,
235 title: None,
236 goal: None,
237 locale: None,
238 tags: Vec::new(),
239 model_id: None,
240 capabilities: Vec::new(),
241 tools: Vec::new(),
242 mcp_servers: ScopedMcpServers::default(),
243 system_prompt: None,
244 initial_files: Vec::new(),
245 hints: None,
246 network_access: None,
247 max_iterations: None,
248 parallel_tool_calls: None,
249 status: SessionExecutionState::Started,
250 usage: None,
251 parent_session_id: None,
252 forked_from_session_id: None,
253 blueprint_id: None,
254 blueprint_config: None,
255 }
256 }
257
258 pub fn with_own_workspace(id: SessionId, harness_id: HarnessId) -> Self {
261 Self::new(id, WorkspaceId::from_uuid(id.uuid()), harness_id)
262 }
263}
264
265#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
267#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
268#[serde(rename_all = "snake_case")]
269pub enum SessionSeedMode {
270 #[default]
272 Fresh,
273 Fork,
275 Workspace,
277}
278
279impl SessionSeedMode {
280 pub fn as_str(self) -> &'static str {
281 match self {
282 Self::Fresh => "fresh",
283 Self::Fork => "fork",
284 Self::Workspace => "workspace",
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn execution_state_preserves_wire_values_and_legacy_inputs() {
295 for (state, wire) in [
296 (SessionExecutionState::Started, "started"),
297 (SessionExecutionState::Active, "active"),
298 (SessionExecutionState::Idle, "idle"),
299 (
300 SessionExecutionState::WaitingForToolResults,
301 "waiting_for_tool_results",
302 ),
303 (SessionExecutionState::Paused, "paused"),
304 ] {
305 assert_eq!(state.as_str(), wire);
306 assert_eq!(state.to_string(), wire);
307 assert_eq!(SessionExecutionState::from(wire), state);
308 assert_eq!(
309 serde_json::to_value(state).unwrap(),
310 serde_json::json!(wire)
311 );
312 assert_eq!(
313 serde_json::from_value::<SessionExecutionState>(serde_json::json!(wire)).unwrap(),
314 state
315 );
316 }
317 for (legacy, expected) in [
318 ("running", SessionExecutionState::Active),
319 ("pending", SessionExecutionState::Idle),
320 ("completed", SessionExecutionState::Idle),
321 ("failed", SessionExecutionState::Idle),
322 ("garbage", SessionExecutionState::Started),
323 ] {
324 assert_eq!(SessionExecutionState::from(legacy), expected, "{legacy}");
325 }
326 }
327
328 #[test]
329 fn execution_session_preserves_minimal_portable_shape() {
330 let session = ExecutionSession::with_own_workspace(
331 "session_01933b5a000070008000000000000001".parse().unwrap(),
332 "harness_01933b5a000070008000000000000002".parse().unwrap(),
333 );
334 assert_eq!(
335 serde_json::to_value(&session).unwrap(),
336 serde_json::json!({
337 "id": "session_01933b5a000070008000000000000001",
338 "organization_id": "org_00000000000000000000000000000001",
339 "workspace_id": "wsp_01933b5a000070008000000000000001",
340 "harness_id": "harness_01933b5a000070008000000000000002",
341 "status": "started",
342 })
343 );
344 }
345}