Skip to main content

af_agent/
prompt.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Who owns a prompt section; higher authority may not be shadowed by lower.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum PromptAuthority {
9    /// Factory runtime.
10    Platform,
11    /// The product.
12    Product,
13    /// The pinned Profile revision.
14    Profile,
15    /// An official Skill.
16    TrustedCapability,
17    /// A trusted plugin.
18    Plugin,
19}
20
21/// One uniquely owned, ordered piece of the system prompt.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct PromptSection {
24    /// Stable identifier of this record.
25    pub id: String,
26    /// Render position; lower renders first.
27    pub order: i32,
28    /// Owner authority.
29    pub authority: PromptAuthority,
30    /// Where this record originated.
31    pub source: String,
32    /// Semantic version string.
33    pub version: String,
34    /// Content blocks carried by this record.
35    pub content: String,
36}
37
38/// Deterministic set of prompt sections keyed by id.
39#[derive(Debug, Clone, Default)]
40pub struct PromptRegistry {
41    sections: BTreeMap<(i32, String), PromptSection>,
42}
43
44impl PromptRegistry {
45    /// Insert a section; a duplicate id is an error.
46    pub fn insert(&mut self, section: PromptSection) -> Result<(), String> {
47        if section.id.trim().is_empty() || section.content.trim().is_empty() {
48            return Err("prompt section id and content are required".into());
49        }
50        if self
51            .sections
52            .values()
53            .any(|current| current.id == section.id)
54        {
55            return Err(format!("duplicate prompt section '{}'", section.id));
56        }
57        self.sections
58            .insert((section.order, section.id.clone()), section);
59        Ok(())
60    }
61
62    /// Render sections in `order` as one system prompt.
63    pub fn render(&self) -> String {
64        self.sections
65            .values()
66            .map(|section| section.content.trim())
67            .collect::<Vec<_>>()
68            .join("\n\n")
69    }
70
71    /// Sections in render order.
72    pub fn sections(&self) -> Vec<PromptSection> {
73        self.sections.values().cloned().collect()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn renders_stably_and_rejects_duplicate_ids() {
83        let mut prompts = PromptRegistry::default();
84        prompts
85            .insert(PromptSection {
86                id: "profile".into(),
87                order: 20,
88                authority: PromptAuthority::Profile,
89                source: "profile".into(),
90                version: "r1".into(),
91                content: "profile".into(),
92            })
93            .unwrap();
94        prompts
95            .insert(PromptSection {
96                id: "base".into(),
97                order: 10,
98                authority: PromptAuthority::Product,
99                source: "product".into(),
100                version: "1".into(),
101                content: "base".into(),
102            })
103            .unwrap();
104        assert_eq!(prompts.render(), "base\n\nprofile");
105        assert!(prompts
106            .insert(PromptSection {
107                id: "base".into(),
108                order: 30,
109                authority: PromptAuthority::Plugin,
110                source: "plugin".into(),
111                version: "1".into(),
112                content: "duplicate".into(),
113            })
114            .is_err());
115    }
116}