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}