Skip to main content

lean_ctx/core/agents/
roles.rs

1use super::ScratchpadEntry;
2use crate::core::a2a::message::{A2AMessage, MessageCategory};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum AgentRole {
8    Coder,
9    Reviewer,
10    Planner,
11    Explorer,
12    Debugger,
13    Tester,
14    Orchestrator,
15}
16
17/// Longest role id, in bytes.
18pub const MAX_ROLE_ID_BYTES: usize = 48;
19
20/// `raw` as a role id (`coder`, `reviewer`, `docs-writer`, …): trimmed and
21/// lower-cased, 1–48 bytes of `[a-z0-9_-]` starting with a letter. Free text
22/// (a session or display name such as "LeanCTX · Contracts") is no role id:
23/// `None`, and the caller names it a display name instead.
24pub fn role_id(raw: &str) -> Option<String> {
25    let id = raw.trim().to_ascii_lowercase();
26    let valid = id.len() <= MAX_ROLE_ID_BYTES
27        && id.bytes().next().is_some_and(|b| b.is_ascii_lowercase())
28        && id
29            .bytes()
30            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-'));
31    valid.then_some(id)
32}
33
34impl AgentRole {
35    pub fn from_str_loose(s: &str) -> Self {
36        match s.to_lowercase().as_str() {
37            "review" | "reviewer" | "code_review" => Self::Reviewer,
38            "plan" | "planner" | "architect" => Self::Planner,
39            "explore" | "explorer" | "research" => Self::Explorer,
40            "debug" | "debugger" => Self::Debugger,
41            "test" | "tester" | "qa" => Self::Tester,
42            "orchestrator" | "coordinator" | "manager" => Self::Orchestrator,
43            _ => Self::Coder,
44        }
45    }
46}
47
48#[derive(Debug, Clone)]
49pub struct ContextDepthConfig {
50    pub max_files_full: usize,
51    pub max_files_signatures: usize,
52    pub preferred_mode: &'static str,
53    pub context_budget_ratio: f64,
54}
55
56impl ContextDepthConfig {
57    pub fn for_role(role: AgentRole) -> Self {
58        match role {
59            AgentRole::Coder => Self {
60                max_files_full: 5,
61                max_files_signatures: 15,
62                preferred_mode: "full",
63                context_budget_ratio: 0.7,
64            },
65            AgentRole::Reviewer => Self {
66                max_files_full: 3,
67                max_files_signatures: 20,
68                preferred_mode: "signatures",
69                context_budget_ratio: 0.5,
70            },
71            AgentRole::Planner => Self {
72                max_files_full: 1,
73                max_files_signatures: 10,
74                preferred_mode: "map",
75                context_budget_ratio: 0.3,
76            },
77            AgentRole::Explorer => Self {
78                max_files_full: 2,
79                max_files_signatures: 8,
80                preferred_mode: "map",
81                context_budget_ratio: 0.4,
82            },
83            AgentRole::Debugger => Self {
84                max_files_full: 8,
85                max_files_signatures: 5,
86                preferred_mode: "full",
87                context_budget_ratio: 0.8,
88            },
89            AgentRole::Tester => Self {
90                max_files_full: 4,
91                max_files_signatures: 10,
92                preferred_mode: "full",
93                context_budget_ratio: 0.6,
94            },
95            AgentRole::Orchestrator => Self {
96                max_files_full: 0,
97                max_files_signatures: 5,
98                preferred_mode: "map",
99                context_budget_ratio: 0.2,
100            },
101        }
102    }
103}
104
105impl From<ScratchpadEntry> for A2AMessage {
106    fn from(entry: ScratchpadEntry) -> Self {
107        Self {
108            id: entry.id,
109            from_agent: entry.from_agent,
110            to_agent: entry.to_agent,
111            task_id: entry.task_id,
112            category: MessageCategory::parse_str(&entry.category),
113            priority: entry.priority,
114            privacy: entry.privacy,
115            content: entry.message,
116            metadata: entry.metadata,
117            project_root: entry.project_root,
118            timestamp: entry.timestamp,
119            read_by: entry.read_by,
120            expires_at: entry.expires_at,
121        }
122    }
123}
124
125impl From<A2AMessage> for ScratchpadEntry {
126    fn from(msg: A2AMessage) -> Self {
127        Self {
128            id: msg.id,
129            from_agent: msg.from_agent,
130            to_agent: msg.to_agent,
131            task_id: msg.task_id,
132            category: msg.category.to_string(),
133            priority: msg.priority,
134            privacy: msg.privacy,
135            message: msg.content,
136            metadata: msg.metadata,
137            project_root: msg.project_root,
138            timestamp: msg.timestamp,
139            read_by: msg.read_by,
140            expires_at: msg.expires_at,
141        }
142    }
143}