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_round_trips_through_its_wire_string() {
295 for state in [
296 SessionExecutionState::Started,
297 SessionExecutionState::Active,
298 SessionExecutionState::Idle,
299 SessionExecutionState::WaitingForToolResults,
300 SessionExecutionState::Paused,
301 ] {
302 assert_eq!(SessionExecutionState::from(state.as_str()), state);
303 }
304 assert_eq!(
306 SessionExecutionState::from("running"),
307 SessionExecutionState::Active
308 );
309 assert_eq!(
310 SessionExecutionState::from("completed"),
311 SessionExecutionState::Idle
312 );
313 assert_eq!(
314 SessionExecutionState::from("garbage"),
315 SessionExecutionState::Started
316 );
317 }
318
319 #[test]
320 fn execution_session_carries_no_persistence_metadata() {
321 let session = ExecutionSession::with_own_workspace(SessionId::new(), HarnessId::new());
322 let json = serde_json::to_value(&session).unwrap();
323 for persistence_field in [
324 "source",
325 "activity",
326 "owner_principal_id",
327 "resolved_owner_user_id",
328 "owner",
329 "effective_owner",
330 "agent_version_id",
331 "agent_identity_id",
332 "preview",
333 "output_preview",
334 "created_at",
335 "updated_at",
336 "started_at",
337 "finished_at",
338 "is_pinned",
339 "archived_at",
340 "active_schedule_count",
341 "event_count",
342 "task_count",
343 "file_count",
344 "features",
345 "forked_from_sequence",
346 ] {
347 assert!(
348 json.get(persistence_field).is_none(),
349 "portable execution session must not expose {persistence_field}"
350 );
351 }
352 }
353}