agent_works/prompt/
mod.rs1mod fragment;
17
18pub use fragment::{FragmentContext, PromptFragment, compose_fragments};
19
20#[derive(Clone)]
26pub struct EnvironmentFragment {
27 pub os: String,
28 pub cwd: String,
29 pub git_branch: Option<String>,
30}
31
32impl EnvironmentFragment {
33 pub fn detect() -> Self {
35 Self {
36 os: std::env::consts::OS.to_string(),
37 cwd: std::env::current_dir()
38 .map(|p| p.display().to_string())
39 .unwrap_or_else(|_| "unknown".to_string()),
40 git_branch: detect_git_branch(),
41 }
42 }
43}
44
45impl PromptFragment for EnvironmentFragment {
46 fn name(&self) -> &str {
47 "environment"
48 }
49
50 fn priority(&self) -> i32 {
51 50
52 }
53
54 fn render(&self, _ctx: &FragmentContext) -> Option<String> {
55 let mut lines = vec![
56 format!("[Environment]\n- OS: {}", self.os),
57 format!("- Working directory: {}", self.cwd),
58 ];
59 if let Some(ref branch) = self.git_branch {
60 lines.push(format!("- Git branch: {}", branch));
61 }
62 Some(lines.join("\n"))
63 }
64}
65
66#[derive(Clone)]
74pub struct DynamicToolsFragment;
75
76impl PromptFragment for DynamicToolsFragment {
77 fn name(&self) -> &str {
78 "dynamic_tools"
79 }
80
81 fn priority(&self) -> i32 {
82 70
83 }
84
85 fn render(&self, ctx: &FragmentContext) -> Option<String> {
86 if ctx.tool_definitions.is_empty() {
87 return None;
88 }
89 let mut lines = vec!["[Available Tools]".to_string()];
90 for def in ctx.tool_definitions {
91 let name = def
92 .get("function")
93 .and_then(|f| f.get("name"))
94 .and_then(|n| n.as_str())
95 .unwrap_or("unknown");
96 let desc = def
97 .get("function")
98 .and_then(|f| f.get("description"))
99 .and_then(|d| d.as_str())
100 .unwrap_or("");
101 if desc.is_empty() {
102 lines.push(format!("- `{}`", name));
103 } else {
104 lines.push(format!("- `{}` — {}", name, desc));
105 }
106 }
107 Some(lines.join("\n"))
108 }
109}
110
111fn detect_git_branch() -> Option<String> {
113 std::process::Command::new("git")
114 .args(["rev-parse", "--abbrev-ref", "HEAD"])
115 .output()
116 .ok()
117 .filter(|o| o.status.success())
118 .and_then(|o| String::from_utf8(o.stdout).ok())
119 .map(|s| s.trim().to_string())
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_environment_fragment_render() {
128 let frag = EnvironmentFragment {
129 os: "macos".into(),
130 cwd: "/Users/test".into(),
131 git_branch: Some("main".into()),
132 };
133 let ctx = FragmentContext {
134 tool_definitions: &[],
135 session_id: "s1",
136 };
137 let output = frag.render(&ctx).unwrap();
138 assert!(output.contains("OS: macos"));
139 assert!(output.contains("/Users/test"));
140 assert!(output.contains("Git branch: main"));
141 }
142
143 #[test]
144 fn test_environment_fragment_no_git() {
145 let frag = EnvironmentFragment {
146 os: "linux".into(),
147 cwd: "/tmp".into(),
148 git_branch: None,
149 };
150 let ctx = FragmentContext {
151 tool_definitions: &[],
152 session_id: "s1",
153 };
154 let output = frag.render(&ctx).unwrap();
155 assert!(output.contains("OS: linux"));
156 assert!(!output.contains("Git branch"));
157 }
158
159 #[test]
160 fn test_dynamic_tools_fragment_with_tools() {
161 let tool_def = serde_json::json!({
162 "type": "function",
163 "function": {
164 "name": "read_file",
165 "description": "Read a file from disk",
166 "parameters": {}
167 }
168 });
169 let frag = DynamicToolsFragment;
170 let ctx = FragmentContext {
171 tool_definitions: &[tool_def],
172 session_id: "test",
173 };
174 let output = frag.render(&ctx).unwrap();
175 assert!(output.contains("read_file"));
176 assert!(output.contains("Read a file from disk"));
177 }
178
179 #[test]
180 fn test_dynamic_tools_fragment_empty() {
181 let frag = DynamicToolsFragment;
182 let ctx = FragmentContext {
183 tool_definitions: &[],
184 session_id: "test",
185 };
186 assert!(frag.render(&ctx).is_none());
187 }
188
189 #[test]
190 fn test_compose_orders_by_priority() {
191 let env = EnvironmentFragment {
192 os: "linux".into(),
193 cwd: "/tmp".into(),
194 git_branch: None,
195 };
196 let tool_def = serde_json::json!({
197 "type": "function",
198 "function": { "name": "test_tool", "description": "A test", "parameters": {} }
199 });
200 let fragments: Vec<Box<dyn PromptFragment>> = vec![
201 Box::new(DynamicToolsFragment), Box::new(env), ];
204 let ctx = FragmentContext {
205 tool_definitions: &[tool_def],
206 session_id: "test",
207 };
208 let result = compose_fragments(&fragments, &ctx);
209 let env_pos = result.find("[Environment]").unwrap();
210 let tools_pos = result.find("[Available Tools]").unwrap();
211 assert!(
212 env_pos < tools_pos,
213 "Environment (50) should come before Tools (70)"
214 );
215 }
216}