Skip to main content

roder_api/
teams.rs

1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3
4use crate::events::{ThreadId, TurnId};
5use crate::policy_mode::PolicyMode;
6
7pub type TeamId = String;
8pub type TeamMemberId = String;
9pub type TeamTaskId = String;
10pub type TeamMessageId = String;
11
12#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
13#[serde(rename_all = "snake_case")]
14pub enum AgentTeamDisplayMode {
15    #[default]
16    Auto,
17    #[serde(alias = "in-process", alias = "inprocess")]
18    InProcess,
19    Tmux,
20    #[serde(alias = "iterm")]
21    Iterm2,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "camelCase")]
26pub struct AgentTeamConfig {
27    pub enabled: bool,
28    pub display_mode: AgentTeamDisplayMode,
29    pub default_teammate_model: Option<String>,
30    pub require_plan_approval: bool,
31    pub max_teammates: usize,
32    pub split_panes: AgentTeamSplitPaneConfig,
33}
34
35impl Default for AgentTeamConfig {
36    fn default() -> Self {
37        Self {
38            enabled: false,
39            display_mode: AgentTeamDisplayMode::Auto,
40            default_teammate_model: Some("lead".to_string()),
41            require_plan_approval: false,
42            max_teammates: 5,
43            split_panes: AgentTeamSplitPaneConfig::default(),
44        }
45    }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "camelCase")]
50pub struct AgentTeamSplitPaneConfig {
51    pub reuse_existing_tmux_session: bool,
52    pub tmux_command: String,
53    pub iterm2_command: String,
54}
55
56impl Default for AgentTeamSplitPaneConfig {
57    fn default() -> Self {
58        Self {
59            reuse_existing_tmux_session: true,
60            tmux_command: "tmux".to_string(),
61            iterm2_command: "it2".to_string(),
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
67#[serde(rename_all = "snake_case")]
68pub enum TeamMemberRole {
69    Lead,
70    Teammate,
71}
72
73#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
74#[serde(rename_all = "snake_case")]
75pub enum TeamMemberStatus {
76    Idle,
77    Running,
78    Blocked,
79    Completed,
80    Failed,
81    Interrupted,
82    Closed,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "camelCase")]
87pub struct TeamMemberDescriptor {
88    pub id: TeamMemberId,
89    pub role: TeamMemberRole,
90    pub name: String,
91    pub thread_id: ThreadId,
92    pub current_turn_id: Option<TurnId>,
93    pub model_provider: Option<String>,
94    pub model: Option<String>,
95    pub policy_mode: PolicyMode,
96    pub status: TeamMemberStatus,
97    pub pane_id: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "camelCase")]
102pub struct TeamMailboxMessage {
103    pub id: TeamMessageId,
104    pub team_id: TeamId,
105    pub from_member_id: Option<TeamMemberId>,
106    pub to_member_id: TeamMemberId,
107    pub text: String,
108    #[serde(with = "time::serde::rfc3339")]
109    pub timestamp: OffsetDateTime,
110}
111
112#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
113#[serde(rename_all = "snake_case")]
114pub enum TeamTaskStatus {
115    Pending,
116    InProgress,
117    Completed,
118    Failed,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(rename_all = "camelCase")]
123pub struct TeamTaskDescriptor {
124    pub id: TeamTaskId,
125    pub title: String,
126    pub status: TeamTaskStatus,
127    pub assignee_member_id: Option<TeamMemberId>,
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn team_display_mode_accepts_snake_case_and_hyphenated_values() {
136        let snake: AgentTeamDisplayMode = serde_json::from_str("\"in_process\"").unwrap();
137        let hyphen: AgentTeamDisplayMode = serde_json::from_str("\"in-process\"").unwrap();
138
139        assert_eq!(snake, AgentTeamDisplayMode::InProcess);
140        assert_eq!(hyphen, AgentTeamDisplayMode::InProcess);
141        assert_eq!(
142            serde_json::to_string(&AgentTeamDisplayMode::Tmux).unwrap(),
143            "\"tmux\""
144        );
145    }
146
147    #[test]
148    fn agent_team_config_defaults_to_disabled_auto() {
149        let config = AgentTeamConfig::default();
150
151        assert!(!config.enabled);
152        assert_eq!(config.display_mode, AgentTeamDisplayMode::Auto);
153        assert_eq!(config.max_teammates, 5);
154        assert!(config.split_panes.reuse_existing_tmux_session);
155    }
156}