agent_works/prompt/
fragment.rs1pub struct FragmentContext<'a> {
5 pub tool_definitions: &'a [serde_json::Value],
7 pub session_id: &'a str,
9}
10
11pub trait PromptFragment: Send + Sync + dyn_clone::DynClone {
27 fn name(&self) -> &str;
29
30 fn priority(&self) -> i32 {
32 100
33 }
34
35 fn render(&self, ctx: &FragmentContext) -> Option<String>;
37}
38
39dyn_clone::clone_trait_object!(PromptFragment);
40
41pub fn compose_fragments(fragments: &[Box<dyn PromptFragment>], ctx: &FragmentContext) -> String {
46 let mut sorted: Vec<&Box<dyn PromptFragment>> = fragments.iter().collect();
47 sorted.sort_by_key(|f| f.priority());
48 sorted
49 .iter()
50 .filter_map(|f| f.render(ctx))
51 .collect::<Vec<_>>()
52 .join("\n\n")
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[derive(Clone)]
60 struct AlphaFragment;
61 impl PromptFragment for AlphaFragment {
62 fn name(&self) -> &str {
63 "alpha"
64 }
65 fn priority(&self) -> i32 {
66 20
67 }
68 fn render(&self, _ctx: &FragmentContext) -> Option<String> {
69 Some("ALPHA".to_string())
70 }
71 }
72
73 #[derive(Clone)]
74 struct BetaFragment;
75 impl PromptFragment for BetaFragment {
76 fn name(&self) -> &str {
77 "beta"
78 }
79 fn priority(&self) -> i32 {
80 10
81 }
82 fn render(&self, _ctx: &FragmentContext) -> Option<String> {
83 Some("BETA".to_string())
84 }
85 }
86
87 #[derive(Clone)]
88 struct SkipFragment;
89 impl PromptFragment for SkipFragment {
90 fn name(&self) -> &str {
91 "skip"
92 }
93 fn render(&self, _ctx: &FragmentContext) -> Option<String> {
94 None
95 }
96 }
97
98 #[test]
99 fn compose_sorts_by_priority() {
100 let fragments: Vec<Box<dyn PromptFragment>> = vec![
101 Box::new(AlphaFragment), Box::new(BetaFragment), ];
104 let ctx = FragmentContext {
105 tool_definitions: &[],
106 session_id: "test",
107 };
108 let result = compose_fragments(&fragments, &ctx);
109 assert_eq!(result, "BETA\n\nALPHA");
111 }
112
113 #[test]
114 fn compose_skips_none() {
115 let fragments: Vec<Box<dyn PromptFragment>> = vec![
116 Box::new(AlphaFragment),
117 Box::new(SkipFragment),
118 Box::new(BetaFragment),
119 ];
120 let ctx = FragmentContext {
121 tool_definitions: &[],
122 session_id: "test",
123 };
124 let result = compose_fragments(&fragments, &ctx);
125 assert_eq!(result, "BETA\n\nALPHA");
126 }
127
128 #[test]
129 fn compose_empty_fragments() {
130 let fragments: Vec<Box<dyn PromptFragment>> = vec![];
131 let ctx = FragmentContext {
132 tool_definitions: &[],
133 session_id: "test",
134 };
135 let result = compose_fragments(&fragments, &ctx);
136 assert!(result.is_empty());
137 }
138
139 #[test]
140 fn compose_all_none_returns_empty() {
141 let fragments: Vec<Box<dyn PromptFragment>> =
142 vec![Box::new(SkipFragment), Box::new(SkipFragment)];
143 let ctx = FragmentContext {
144 tool_definitions: &[],
145 session_id: "test",
146 };
147 let result = compose_fragments(&fragments, &ctx);
148 assert!(result.is_empty());
149 }
150
151 #[test]
152 fn default_priority_is_100() {
153 #[derive(Clone)]
154 struct DefaultPriorityFragment;
155 impl PromptFragment for DefaultPriorityFragment {
156 fn name(&self) -> &str {
157 "default"
158 }
159 fn render(&self, _ctx: &FragmentContext) -> Option<String> {
160 Some("default".to_string())
161 }
162 }
163 assert_eq!(DefaultPriorityFragment.priority(), 100);
164 }
165}