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}