Skip to main content

tycode_core/
module.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use schemars::schema::RootSchema;
5use serde_json::Value;
6
7use crate::chat::actor::ActorState;
8use crate::chat::events::ChatMessage;
9use crate::settings::config::Settings;
10use crate::tools::r#trait::ToolExecutor;
11
12/// Strongly-typed identifier for prompt components.
13/// Using a wrapper type prevents accidental hardcoding of strings
14/// and ensures compile-time checking of component references.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct PromptComponentId(pub &'static str);
17
18/// Selection strategy for which prompt components an agent wants included.
19///
20/// Prompt components contribute to the system prompt - the initial instructions
21/// given to the AI that shape its behavior. Examples include style mandates,
22/// autonomy level instructions, tool usage guidelines, etc.
23///
24/// Most agents want all components, but specialized agents (like memory manager)
25/// may want to exclude certain components (like autonomy instructions) because
26/// they have their own bespoke prompts.
27#[derive(Clone, Copy, Debug)]
28pub enum PromptComponentSelection {
29    /// Include all available prompt components
30    All,
31    /// Include only the specified prompt components
32    Only(&'static [PromptComponentId]),
33    /// Include all except the specified prompt components
34    Exclude(&'static [PromptComponentId]),
35    /// Exclude all prompt components (agent has its own complete prompt)
36    None,
37}
38
39/// A composable unit that contributes to the system prompt.
40/// Implementations provide specific sections of prompt content.
41pub trait PromptComponent: Send + Sync {
42    /// Returns the unique identifier for this component.
43    /// This ID is used for filtering via PromptComponentSelection.
44    fn id(&self) -> PromptComponentId;
45
46    /// Returns the prompt section content, or None if this component
47    /// should not contribute to the current prompt.
48    fn build_prompt_section(&self, settings: &Settings) -> Option<String>;
49}
50
51// === Session State ===
52
53/// Handles session persistence for a module's state.
54///
55/// Modules that need to persist state across sessions should return
56/// an implementation of this trait from `Module::session_state()`.
57pub trait SessionStateComponent: Send + Sync {
58    /// Unique key for storing this module's state in session data.
59    fn key(&self) -> &str;
60
61    /// Serialize current state for persistence.
62    fn save(&self) -> Value;
63
64    /// Restore state from persisted session data.
65    fn load(&self, state: Value) -> Result<()>;
66}
67
68/// A slash command that can be provided by a module.
69/// Modules implement this trait for commands they want to register.
70#[async_trait::async_trait(?Send)]
71pub trait SlashCommand: Send + Sync {
72    /// The command name without the leading slash (e.g., "memory" for /memory)
73    fn name(&self) -> &'static str;
74
75    /// Short description shown in help
76    fn description(&self) -> &'static str;
77
78    /// Usage example shown in help (e.g., "/memory summarize")
79    fn usage(&self) -> &'static str;
80
81    /// Whether to hide this command from /help output
82    fn hidden(&self) -> bool {
83        false
84    }
85
86    /// Execute the command with the given arguments
87    async fn execute(&self, state: &mut ActorState, args: &[&str]) -> Vec<ChatMessage>;
88}
89
90/// A Module bundles related prompt components, context components, and tools.
91///
92/// Modules represent cohesive functionality that spans multiple systems:
93/// - Prompts: Instructions for how the agent should behave
94/// - Context: Runtime state included in each request
95/// - Tools: Actions the agent can take
96///
97/// Example: TaskListModule provides task tracking across all three:
98/// - Prompt instructions for managing tasks
99/// - Context showing current task status
100/// - Tools to create and update tasks
101pub trait Module: Send + Sync {
102    fn prompt_components(&self) -> Vec<Arc<dyn PromptComponent>>;
103    fn context_components(&self) -> Vec<Arc<dyn ContextComponent>>;
104    fn tools(&self) -> Vec<Arc<dyn ToolExecutor>>;
105
106    /// Returns a session state component if this module has persistent state.
107    /// Return None if this module has no state to persist across sessions.
108    fn session_state(&self) -> Option<Arc<dyn SessionStateComponent>> {
109        None
110    }
111
112    /// Returns slash commands provided by this module.
113    /// Default implementation returns an empty vec (no commands).
114    fn slash_commands(&self) -> Vec<Arc<dyn SlashCommand>> {
115        vec![]
116    }
117
118    /// Option allows modules without configuration to opt-out, avoiding empty entries.
119    fn settings_namespace(&self) -> Option<&'static str> {
120        None
121    }
122
123    /// Returns JSON Schema for this module's settings configuration.
124    /// Used for auto-generating settings UI.
125    fn settings_json_schema(&self) -> Option<RootSchema> {
126        None
127    }
128}
129
130/// Encapsulates prompt component management and builds the combined prompt.
131#[derive(Clone)]
132pub struct PromptBuilder {
133    components: Vec<Arc<dyn PromptComponent>>,
134}
135
136impl PromptBuilder {
137    pub fn new() -> Self {
138        Self {
139            components: Vec::new(),
140        }
141    }
142
143    pub fn add(&mut self, component: Arc<dyn PromptComponent>) {
144        self.components.push(component);
145    }
146
147    /// Builds prompt sections filtered by the given selection, including components from modules.
148    pub fn build(
149        &self,
150        settings: &Settings,
151        selection: &PromptComponentSelection,
152        modules: &[Arc<dyn Module>],
153    ) -> String {
154        let module_components: Vec<Arc<dyn PromptComponent>> =
155            modules.iter().flat_map(|m| m.prompt_components()).collect();
156
157        let all_components: Vec<&Arc<dyn PromptComponent>> = self
158            .components
159            .iter()
160            .chain(module_components.iter())
161            .collect();
162
163        if all_components.is_empty() {
164            return String::new();
165        }
166
167        let sections: Vec<String> = all_components
168            .iter()
169            .filter(|c| match selection {
170                PromptComponentSelection::All => true,
171                PromptComponentSelection::Only(ids) => ids.contains(&c.id()),
172                PromptComponentSelection::Exclude(ids) => !ids.contains(&c.id()),
173                PromptComponentSelection::None => false,
174            })
175            .filter_map(|c| c.build_prompt_section(settings))
176            .collect();
177
178        if sections.is_empty() {
179            String::new()
180        } else {
181            format!("\n\n{}", sections.join("\n\n"))
182        }
183    }
184}
185
186impl Default for PromptBuilder {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192// === Context Components ===
193
194/// Strongly-typed identifier for context components.
195/// Using a wrapper type prevents accidental hardcoding of strings
196/// and ensures compile-time checking of component references.
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub struct ContextComponentId(pub &'static str);
199
200/// Selection strategy for which context components an agent wants included.
201///
202/// Context components contribute to "continuous steering" - a feature where
203/// the last message to the agent always contains fresh, up-to-date context.
204/// Examples include:
205/// - File tree listing (project structure)
206/// - Tracked file contents (full source code of relevant files)
207/// - Memory log (user preferences, past corrections)
208/// - Task list (current work items and status)
209///
210/// All context is refreshed on each request, ensuring the agent never sees
211/// stale file contents, outdated task lists, etc.
212///
213/// Most agents benefit from all context, but specialized agents may want
214/// fine-grained control. For example, an agent focused on a specific task
215/// might exclude irrelevant context to reduce noise.
216#[derive(Clone, Copy, Debug)]
217pub enum ContextComponentSelection {
218    /// Include all available context components
219    All,
220    /// Include only the specified context components
221    Only(&'static [ContextComponentId]),
222    /// Include all except the specified context components
223    Exclude(&'static [ContextComponentId]),
224    /// Exclude all context components
225    None,
226}
227
228/// A composable unit that contributes to the context message.
229/// Implementations provide specific sections of context content.
230/// Components should be self-contained - owning any state they need.
231#[async_trait::async_trait(?Send)]
232pub trait ContextComponent: Send + Sync {
233    /// Returns the unique identifier for this component.
234    /// This ID is used for filtering via ContextComponentSelection.
235    fn id(&self) -> ContextComponentId;
236
237    /// Returns the context section content, or None if this component
238    /// should not contribute to the current context.
239    async fn build_context_section(&self) -> Option<String>;
240}
241
242/// Encapsulates context component management and builds combined context sections.
243#[derive(Clone)]
244pub struct ContextBuilder {
245    components: Vec<Arc<dyn ContextComponent>>,
246}
247
248impl ContextBuilder {
249    pub fn new() -> Self {
250        Self {
251            components: Vec::new(),
252        }
253    }
254
255    pub fn add(&mut self, component: Arc<dyn ContextComponent>) {
256        self.components.push(component);
257    }
258
259    /// Builds context sections filtered by the given selection, including components from modules.
260    pub async fn build(
261        &self,
262        selection: &ContextComponentSelection,
263        modules: &[Arc<dyn Module>],
264    ) -> String {
265        let module_components: Vec<Arc<dyn ContextComponent>> = modules
266            .iter()
267            .flat_map(|m| m.context_components())
268            .collect();
269
270        let all_components: Vec<&Arc<dyn ContextComponent>> = self
271            .components
272            .iter()
273            .chain(module_components.iter())
274            .collect();
275
276        if all_components.is_empty() {
277            return String::new();
278        }
279
280        let filtered: Vec<_> = all_components
281            .iter()
282            .filter(|c| match selection {
283                ContextComponentSelection::All => true,
284                ContextComponentSelection::Only(ids) => ids.contains(&c.id()),
285                ContextComponentSelection::Exclude(ids) => !ids.contains(&c.id()),
286                ContextComponentSelection::None => false,
287            })
288            .collect();
289
290        let mut sections = Vec::new();
291        for component in filtered {
292            if let Some(section) = component.build_context_section().await {
293                sections.push(section);
294            }
295        }
296
297        if sections.is_empty() {
298            String::new()
299        } else {
300            format!("\n\n{}", sections.join("\n"))
301        }
302    }
303}
304
305impl Default for ContextBuilder {
306    fn default() -> Self {
307        Self::new()
308    }
309}