1use agent_settings::AgentProfileId;
  2use anyhow::Result;
  3use async_trait::async_trait;
  4use serde::Deserialize;
  5use std::collections::BTreeMap;
  6use std::fs;
  7use std::{
  8    path::{Path, PathBuf},
  9    rc::Rc,
 10};
 11use util::serde::default_true;
 12
 13use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
 14
 15mod add_arg_to_trait_method;
 16mod code_block_citations;
 17mod comment_translation;
 18mod file_change_notification;
 19mod file_search;
 20mod grep_params_escapement;
 21mod overwrite_file;
 22mod planets;
 23
 24pub fn all(examples_dir: &Path) -> Vec<Rc<dyn Example>> {
 25    let mut threads: Vec<Rc<dyn Example>> = vec![
 26        Rc::new(file_search::FileSearchExample),
 27        Rc::new(add_arg_to_trait_method::AddArgToTraitMethod),
 28        Rc::new(code_block_citations::CodeBlockCitations),
 29        Rc::new(planets::Planets),
 30        Rc::new(comment_translation::CommentTranslation),
 31        Rc::new(overwrite_file::FileOverwriteExample),
 32        Rc::new(file_change_notification::FileChangeNotificationExample),
 33        Rc::new(grep_params_escapement::GrepParamsEscapementExample),
 34    ];
 35
 36    for example_path in list_declarative_examples(examples_dir).unwrap() {
 37        threads.push(Rc::new(DeclarativeExample::load(&example_path).unwrap()));
 38    }
 39
 40    threads
 41}
 42
 43struct DeclarativeExample {
 44    metadata: ExampleMetadata,
 45    prompt: String,
 46    diff_assertions: Vec<JudgeAssertion>,
 47    thread_assertions: Vec<JudgeAssertion>,
 48}
 49
 50impl DeclarativeExample {
 51    pub fn load(example_path: &Path) -> Result<Self> {
 52        let name = Self::name_from_path(example_path);
 53        let base: ExampleToml = toml::from_str(&fs::read_to_string(&example_path)?)?;
 54        let example_dir = example_path.parent().unwrap();
 55
 56        let language_server = if base.require_lsp {
 57            Some(crate::example::LanguageServer {
 58                file_extension: base
 59                    .language_extension
 60                    .expect("Language extension is required when require_lsp = true"),
 61                allow_preexisting_diagnostics: base.allow_preexisting_diagnostics,
 62            })
 63        } else {
 64            None
 65        };
 66
 67        let profile_id = if let Some(profile_name) = base.profile_name {
 68            AgentProfileId(profile_name.into())
 69        } else {
 70            AgentProfileId::default()
 71        };
 72
 73        let existing_thread_json = if let Some(path) = base.existing_thread_path {
 74            let content = fs::read_to_string(example_dir.join(&path))
 75                .unwrap_or_else(|_| panic!("Failed to read existing thread file: {}", path));
 76            Some(content)
 77        } else {
 78            None
 79        };
 80
 81        let metadata = ExampleMetadata {
 82            name,
 83            url: base.url,
 84            revision: base.revision,
 85            language_server,
 86            max_assertions: None,
 87            profile_id,
 88            existing_thread_json,
 89            max_turns: base.max_turns,
 90        };
 91
 92        Ok(DeclarativeExample {
 93            metadata,
 94            prompt: base.prompt,
 95            thread_assertions: base
 96                .thread_assertions
 97                .into_iter()
 98                .map(|(id, description)| JudgeAssertion { id, description })
 99                .collect(),
100            diff_assertions: base
101                .diff_assertions
102                .into_iter()
103                .map(|(id, description)| JudgeAssertion { id, description })
104                .collect(),
105        })
106    }
107
108    pub fn name_from_path(path: &Path) -> String {
109        path.file_stem().unwrap().to_string_lossy().into_owned()
110    }
111}
112
113#[derive(Clone, Debug, Deserialize)]
114pub struct ExampleToml {
115    pub url: String,
116    pub revision: String,
117    pub language_extension: Option<String>,
118    #[expect(
119        unused,
120        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
121    )]
122    pub insert_id: Option<String>,
123    #[serde(default = "default_true")]
124    pub require_lsp: bool,
125    #[serde(default)]
126    pub allow_preexisting_diagnostics: bool,
127    pub prompt: String,
128    #[serde(default)]
129    pub profile_name: Option<String>,
130    #[serde(default)]
131    pub diff_assertions: BTreeMap<String, String>,
132    #[serde(default)]
133    pub thread_assertions: BTreeMap<String, String>,
134    #[serde(default)]
135    pub existing_thread_path: Option<String>,
136    #[serde(default)]
137    pub max_turns: Option<u32>,
138}
139
140#[async_trait(?Send)]
141impl Example for DeclarativeExample {
142    fn meta(&self) -> ExampleMetadata {
143        self.metadata.clone()
144    }
145
146    async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> {
147        let max_turns = self.metadata.max_turns.unwrap_or(1000);
148        let _ = cx.prompt_with_max_turns(&self.prompt, max_turns).await;
149        Ok(())
150    }
151
152    fn diff_assertions(&self) -> Vec<JudgeAssertion> {
153        self.diff_assertions.clone()
154    }
155
156    fn thread_assertions(&self) -> Vec<JudgeAssertion> {
157        self.thread_assertions.clone()
158    }
159}
160
161fn list_declarative_examples(examples_dir: &Path) -> Result<Vec<PathBuf>> {
162    let path = std::fs::canonicalize(examples_dir).unwrap();
163    let entries = std::fs::read_dir(path).unwrap();
164    let mut result_paths = Vec::new();
165    for entry in entries {
166        let entry = entry?;
167        let path = entry.path();
168        if path.extension() == Some("toml".as_ref()) {
169            result_paths.push(path);
170        }
171    }
172    Ok(result_paths)
173}