1use 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#[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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
77#[cfg_attr(feature = "openapi", derive(ToSchema))]
78#[serde(rename_all = "lowercase")]
79pub enum SessionStatus {
80 Started,
82 Active,
84 Idle,
86 WaitingForToolResults,
88 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 "running" => SessionStatus::Active,
113 "pending" | "completed" | "failed" => SessionStatus::Idle,
114 _ => SessionStatus::Started,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
128#[cfg_attr(feature = "openapi", derive(ToSchema))]
129#[serde(rename_all = "snake_case")]
130pub enum SessionSource {
131 Chat,
133 Api,
135 Slack,
137 AgUi,
139 Fcp,
141 Schedule,
143 Webhook,
145 A2a,
147 Eval,
149 Subagent,
151 #[default]
153 Unknown,
154}
155
156impl SessionSource {
157 pub const ALL: &'static [SessionSource] = &[
158 SessionSource::Chat,
159 SessionSource::Api,
160 SessionSource::Slack,
161 SessionSource::AgUi,
162 SessionSource::Fcp,
163 SessionSource::Schedule,
164 SessionSource::Webhook,
165 SessionSource::A2a,
166 SessionSource::Eval,
167 SessionSource::Subagent,
168 SessionSource::Unknown,
169 ];
170
171 pub fn as_str(self) -> &'static str {
172 match self {
173 SessionSource::Chat => "chat",
174 SessionSource::Api => "api",
175 SessionSource::Slack => "slack",
176 SessionSource::AgUi => "ag_ui",
177 SessionSource::Fcp => "fcp",
178 SessionSource::Schedule => "schedule",
179 SessionSource::Webhook => "webhook",
180 SessionSource::A2a => "a2a",
181 SessionSource::Eval => "eval",
182 SessionSource::Subagent => "subagent",
183 SessionSource::Unknown => "unknown",
184 }
185 }
186
187 pub fn parse(s: &str) -> Option<Self> {
188 Self::ALL.iter().copied().find(|v| v.as_str() == s)
189 }
190
191 pub fn is_client_declarable(self) -> bool {
194 matches!(self, SessionSource::Chat | SessionSource::Api)
195 }
196}
197
198impl std::fmt::Display for SessionSource {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.write_str(self.as_str())
201 }
202}
203
204impl From<&str> for SessionSource {
205 fn from(s: &str) -> Self {
206 Self::parse(s).unwrap_or(SessionSource::Unknown)
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
214#[cfg_attr(feature = "openapi", derive(ToSchema))]
215#[serde(rename_all = "snake_case")]
216pub enum SessionActivity {
217 Running,
219 Paused,
221 Failed,
223 Completed,
225 #[default]
227 Idle,
228}
229
230impl SessionActivity {
231 pub const ALL: &'static [SessionActivity] = &[
232 SessionActivity::Running,
233 SessionActivity::Paused,
234 SessionActivity::Failed,
235 SessionActivity::Completed,
236 SessionActivity::Idle,
237 ];
238
239 pub fn as_str(self) -> &'static str {
240 match self {
241 SessionActivity::Running => "running",
242 SessionActivity::Paused => "paused",
243 SessionActivity::Failed => "failed",
244 SessionActivity::Completed => "completed",
245 SessionActivity::Idle => "idle",
246 }
247 }
248
249 pub fn parse(s: &str) -> Option<Self> {
250 Self::ALL.iter().copied().find(|v| v.as_str() == s)
251 }
252
253 pub fn derive(status: &SessionStatus, last_turn_status: Option<&str>) -> Self {
257 match status {
258 SessionStatus::Active | SessionStatus::WaitingForToolResults => {
259 SessionActivity::Running
260 }
261 SessionStatus::Paused => SessionActivity::Paused,
262 _ => match last_turn_status {
263 Some("failed") | Some("cancelled") => SessionActivity::Failed,
264 Some("completed") => SessionActivity::Completed,
265 _ => SessionActivity::Idle,
266 },
267 }
268 }
269}
270
271impl std::fmt::Display for SessionActivity {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 f.write_str(self.as_str())
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279#[cfg_attr(feature = "openapi", derive(ToSchema))]
280#[serde(rename_all = "snake_case")]
281pub enum SessionParticipantKind {
282 Agent,
283 User,
284}
285
286impl std::fmt::Display for SessionParticipantKind {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 SessionParticipantKind::Agent => write!(f, "agent"),
290 SessionParticipantKind::User => write!(f, "user"),
291 }
292 }
293}
294
295impl From<&str> for SessionParticipantKind {
296 fn from(s: &str) -> Self {
297 match s {
298 "agent" => SessionParticipantKind::Agent,
299 "user" => SessionParticipantKind::User,
300 _ => SessionParticipantKind::User,
301 }
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
307#[cfg_attr(feature = "openapi", derive(ToSchema))]
308#[serde(rename_all = "snake_case")]
309pub enum SessionParticipantRole {
310 Host,
311 Member,
312}
313
314impl std::fmt::Display for SessionParticipantRole {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 match self {
317 SessionParticipantRole::Host => write!(f, "host"),
318 SessionParticipantRole::Member => write!(f, "member"),
319 }
320 }
321}
322
323impl From<&str> for SessionParticipantRole {
324 fn from(s: &str) -> Self {
325 match s {
326 "host" => SessionParticipantRole::Host,
327 "member" => SessionParticipantRole::Member,
328 _ => SessionParticipantRole::Member,
329 }
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335#[cfg_attr(feature = "openapi", derive(ToSchema))]
336pub struct SessionParticipant {
337 #[cfg_attr(
339 feature = "openapi",
340 schema(
341 value_type = String,
342 example = "part_01933b5a00007000800000000000001"
343 )
344 )]
345 pub id: SessionParticipantId,
346 #[cfg_attr(
348 feature = "openapi",
349 schema(
350 value_type = String,
351 example = "session_01933b5a00007000800000000000001"
352 )
353 )]
354 pub session_id: SessionId,
355 pub kind: SessionParticipantKind,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 #[cfg_attr(
359 feature = "openapi",
360 schema(
361 value_type = Option<String>,
362 example = "agent_01933b5a00007000800000000000001"
363 )
364 )]
365 pub agent_id: Option<AgentId>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 #[cfg_attr(
369 feature = "openapi",
370 schema(
371 value_type = Option<String>,
372 example = "agentver_01933b5a00007000800000000000001"
373 )
374 )]
375 pub agent_version_id: Option<AgentVersionId>,
376 #[cfg_attr(
378 feature = "openapi",
379 schema(
380 value_type = String,
381 example = "principal_01933b5a000070008000000000000001"
382 )
383 )]
384 pub principal_id: PrincipalId,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub display_name: Option<String>,
388 pub role: SessionParticipantRole,
389 pub joined_at: DateTime<Utc>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub left_at: Option<DateTime<Utc>>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
397#[cfg_attr(feature = "openapi", derive(ToSchema))]
398pub struct Session {
399 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
401 pub id: SessionId,
402 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "org_00000000000000000000000000000001"))]
404 pub organization_id: String,
405 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "wsp_01933b5a00007000800000000000001"))]
409 pub workspace_id: WorkspaceId,
410 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
412 pub harness_id: HarnessId,
413 #[serde(skip_serializing_if = "Option::is_none")]
415 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
416 pub agent_id: Option<AgentId>,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agentver_01933b5a00007000800000000000001"))]
420 pub agent_version_id: Option<AgentVersionId>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "identity_01933b5a00007000800000000000001"))]
424 pub agent_identity_id: Option<AgentIdentityId>,
425 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "principal_01933b5a000070008000000000000001"))]
427 pub owner_principal_id: PrincipalId,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 #[cfg_attr(
431 feature = "openapi",
432 schema(example = "550e8400-e29b-41d4-a716-446655440000")
433 )]
434 pub resolved_owner_user_id: Option<uuid::Uuid>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub owner: Option<PrincipalSummary>,
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub effective_owner: Option<PrincipalSummary>,
441 #[serde(skip_serializing_if = "Option::is_none")]
443 #[cfg_attr(feature = "openapi", schema(example = "Q3 marketing brief"))]
444 pub title: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 #[cfg_attr(
448 feature = "openapi",
449 schema(example = "Investigate the queue latency regression")
450 )]
451 pub goal: Option<String>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 #[cfg_attr(feature = "openapi", schema(example = "en-US"))]
455 pub locale: Option<String>,
456 #[serde(skip_serializing_if = "Option::is_none")]
458 #[cfg_attr(
459 feature = "openapi",
460 schema(example = "Help me draft the Q3 marketing plan")
461 )]
462 pub preview: Option<String>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 #[cfg_attr(
466 feature = "openapi",
467 schema(example = "Here is a Q3 plan covering the three pillars we discussed...")
468 )]
469 pub output_preview: Option<String>,
470 #[serde(default)]
472 #[cfg_attr(feature = "openapi", schema(example = json!(["marketing", "q3", "draft"])))]
473 pub tags: Vec<String>,
474 #[serde(skip_serializing_if = "Option::is_none")]
477 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
478 pub model_id: Option<ModelId>,
479 #[serde(default, skip_serializing_if = "Vec::is_empty")]
482 pub capabilities: Vec<AgentCapabilityConfig>,
483 #[serde(default, skip_serializing_if = "Vec::is_empty")]
485 pub tools: Vec<ToolDefinition>,
486 #[serde(
488 default,
489 rename = "mcpServers",
490 alias = "mcp_servers",
491 skip_serializing_if = "scoped_mcp_servers_is_empty"
492 )]
493 pub mcp_servers: ScopedMcpServers,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub system_prompt: Option<String>,
498 #[serde(default, skip_serializing_if = "Vec::is_empty")]
501 pub initial_files: Vec<crate::session_file::InitialFile>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub hints: Option<std::collections::HashMap<String, serde_json::Value>>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub network_access: Option<NetworkAccessList>,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 #[cfg_attr(feature = "openapi", schema(example = 50))]
516 pub max_iterations: Option<usize>,
517 #[serde(default, skip_serializing_if = "Option::is_none")]
524 #[cfg_attr(feature = "openapi", schema(example = true))]
525 pub parallel_tool_calls: Option<bool>,
526 pub status: SessionStatus,
528 #[serde(default)]
530 pub source: SessionSource,
531 #[serde(default)]
534 pub activity: SessionActivity,
535 #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:00Z"))]
537 pub created_at: DateTime<Utc>,
538 #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
540 pub updated_at: DateTime<Utc>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:00:01Z"))]
544 pub started_at: Option<DateTime<Utc>>,
545 #[serde(skip_serializing_if = "Option::is_none")]
547 #[cfg_attr(feature = "openapi", schema(example = "2026-05-25T10:14:32Z"))]
548 pub finished_at: Option<DateTime<Utc>>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub usage: Option<TokenUsage>,
552 #[serde(skip_serializing_if = "Option::is_none")]
555 #[cfg_attr(feature = "openapi", schema(example = false))]
556 pub is_pinned: Option<bool>,
557 #[serde(skip_serializing_if = "Option::is_none")]
560 #[cfg_attr(feature = "openapi", schema(example = 2))]
561 pub active_schedule_count: Option<u32>,
562 #[serde(default, skip_serializing_if = "Vec::is_empty")]
567 #[cfg_attr(feature = "openapi", schema(example = json!(["file_system", "secrets"])))]
568 pub features: Vec<String>,
569
570 #[serde(skip_serializing_if = "Option::is_none")]
574 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
575 pub parent_session_id: Option<SessionId>,
576
577 #[serde(default, skip_serializing_if = "Option::is_none")]
582 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
583 pub forked_from_session_id: Option<SessionId>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
587 #[cfg_attr(feature = "openapi", schema(example = 42))]
588 pub forked_from_sequence: Option<i32>,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
594 #[cfg_attr(feature = "openapi", schema(example = "blueprint_research_pack"))]
595 pub blueprint_id: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none")]
599 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
600 pub blueprint_config: Option<serde_json::Value>,
601}
602
603#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
605#[cfg_attr(feature = "openapi", derive(ToSchema))]
606#[serde(rename_all = "snake_case")]
607pub enum SessionSeedMode {
608 #[default]
610 Fresh,
611 Fork,
613 Workspace,
615}
616
617impl SessionSeedMode {
618 pub fn as_str(self) -> &'static str {
619 match self {
620 Self::Fresh => "fresh",
621 Self::Fork => "fork",
622 Self::Workspace => "workspace",
623 }
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630
631 #[test]
636 fn activity_derivation_truth_table() {
637 use SessionActivity as A;
638 use SessionStatus as S;
639
640 let cases: &[(SessionStatus, Option<&str>, SessionActivity)] = &[
641 (S::Active, None, A::Running),
644 (S::Active, Some("failed"), A::Running),
645 (S::WaitingForToolResults, Some("completed"), A::Running),
646 (S::Paused, Some("failed"), A::Paused),
647 (S::Idle, Some("completed"), A::Completed),
649 (S::Idle, Some("failed"), A::Failed),
650 (S::Idle, Some("cancelled"), A::Failed),
651 (S::Started, Some("completed"), A::Completed),
652 (S::Started, None, A::Idle),
654 (S::Idle, None, A::Idle),
655 (S::Idle, Some("weird"), A::Idle),
656 ];
657
658 for (status, last_turn, expected) in cases {
659 assert_eq!(
660 SessionActivity::derive(status, *last_turn),
661 *expected,
662 "status={status} last_turn={last_turn:?}"
663 );
664 }
665 }
666
667 #[test]
668 fn session_source_round_trips_through_its_wire_string() {
669 for source in SessionSource::ALL {
670 assert_eq!(SessionSource::parse(source.as_str()), Some(*source));
671 }
672 assert_eq!(SessionSource::from("not_a_source"), SessionSource::Unknown);
675 }
676
677 #[test]
678 fn only_chat_and_api_are_client_declarable() {
679 let declarable: Vec<_> = SessionSource::ALL
680 .iter()
681 .filter(|s| s.is_client_declarable())
682 .copied()
683 .collect();
684 assert_eq!(declarable, vec![SessionSource::Chat, SessionSource::Api]);
685 }
686
687 #[test]
688 fn session_activity_round_trips_through_its_wire_string() {
689 for activity in SessionActivity::ALL {
690 assert_eq!(SessionActivity::parse(activity.as_str()), Some(*activity));
691 }
692 assert_eq!(SessionActivity::parse("nope"), None);
693 }
694}