1216 lines · 42.8 KB · Rust
RawDownload
use agent::{Message, MessageSegment, SerializedThread, ThreadStore};use anyhow::{Context as _, Result, anyhow, bail};use assistant_tool::ToolWorkingSet;use client::proto::LspWorkProgress;use futures::channel::mpsc;use futures::{FutureExt as _, StreamExt as _, future};use gpui::{App, AppContext as _, AsyncApp, Entity, Task};use handlebars::Handlebars;use language::{Buffer, DiagnosticSeverity, OffsetRangeExt as _};use language_model::{    LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage,    LanguageModelToolResultContent, MessageContent, Role, TokenUsage,};use project::lsp_store::OpenLspBufferHandle;use project::{DiagnosticSummary, Project, ProjectPath};use serde::{Deserialize, Serialize};use std::cell::RefCell;use std::fmt::Write as _;use std::fs;use std::fs::File;use std::io::Write as _;use std::path::Path;use std::path::PathBuf;use std::rc::Rc;use std::sync::Arc;use std::time::Duration;use unindent::Unindent as _;use util::ResultExt as _;use util::command::new_smol_command;use util::markdown::MarkdownCodeBlock; use crate::assertions::{AssertionsReport, RanAssertion, RanAssertionResult};use crate::example::{Example, ExampleContext, FailedAssertion, JudgeAssertion};use crate::{AgentAppState, ToolMetrics}; pub const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed.git"; #[derive(Clone)]pub struct ExampleInstance {    pub thread: Rc<dyn Example>,    pub name: String,    pub run_directory: PathBuf,    pub log_prefix: String,    /// The repetition number for this example (0-based)    /// When running multiple repetitions of the same example, each instance is assigned a unique repetition number.    /// This affects the worktree path and log prefix to avoid clobbering results between runs.    pub repetition: usize,    pub repo_path: PathBuf,    /// Path to the directory containing the requests and responses for the agentic loop    worktrees_dir: PathBuf,} #[derive(Debug, Serialize, Clone)]pub struct RunOutput {    pub repository_diff: String,    pub diagnostic_summary_before: DiagnosticSummary,    pub diagnostic_summary_after: DiagnosticSummary,    pub diagnostics_before: Option<String>,    pub diagnostics_after: Option<String>,    pub response_count: usize,    pub token_usage: TokenUsage,    pub tool_metrics: ToolMetrics,    pub all_messages: String,    pub programmatic_assertions: AssertionsReport,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct JudgeDiffInput {    pub repository_diff: String,    pub assertion: String,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct JudgeThreadInput {    pub messages: String,    pub assertion: String,} #[derive(Debug, Clone, Serialize, Deserialize)]pub struct JudgeOutput {    pub thread: AssertionsReport,    pub diff: AssertionsReport,} impl ExampleInstance {    pub fn new(        thread: Rc<dyn Example>,        repos_dir: &Path,        run_dir: &Path,        worktrees_dir: &Path,        repetition: usize,    ) -> Self {        let name = thread.meta().name.to_string();        let run_directory = run_dir            .join(&name)            .join(repetition.to_string())            .to_path_buf();         let repo_path = repo_path_for_url(repos_dir, &thread.meta().url);         Self {            name,            thread,            log_prefix: String::new(),            run_directory,            repetition,            repo_path,            worktrees_dir: worktrees_dir.to_path_buf(),        }    }     pub fn repo_url(&self) -> String {        self.thread.meta().url    }     pub fn revision(&self) -> String {        self.thread.meta().revision    }     pub fn worktree_name(&self) -> String {        format!("{}-{}", self.name, self.repetition)    }     pub fn set_log_prefix_style(&mut self, color: &str, name_width: usize) {        self.log_prefix = format!(            "{}{:<width$}\x1b[0m | ",            color,            self.worktree_name(),            width = name_width        );    }     /// Set up the example by checking out the specified Git revision    pub async fn fetch(&mut self) -> Result<()> {        let meta = self.thread.meta();         let revision_exists = run_git(            &self.repo_path,            &["rev-parse", &format!("{}^{{commit}}", &meta.revision)],        )        .await        .is_ok();         if !revision_exists {            println!("{}Fetching revision {}", self.log_prefix, &meta.revision);            run_git(                &self.repo_path,                &["fetch", "--depth", "1", "origin", &meta.revision],            )            .await?;        }        Ok(())    }     /// Set up the example by checking out the specified Git revision    pub async fn setup(&mut self) -> Result<()> {        let worktree_path = self.worktree_path();        let meta = self.thread.meta();        if worktree_path.is_dir() {            println!("{}Resetting existing worktree", self.log_prefix);             // TODO: consider including "-x" to remove ignored files. The downside of this is that            // it will also remove build artifacts, and so prevent incremental reuse there.            run_git(&worktree_path, &["clean", "--force", "-d"]).await?;            run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?;            run_git(&worktree_path, &["checkout", &meta.revision]).await?;        } else {            println!("{}Creating worktree", self.log_prefix);             let worktree_path_string = worktree_path.to_string_lossy().to_string();             run_git(                &self.repo_path,                &[                    "worktree",                    "add",                    "-f",                    &worktree_path_string,                    &meta.revision,                ],            )            .await?;        }         if meta.url == ZED_REPO_URL {            std::fs::write(worktree_path.join(".rules"), std::fs::read(".rules")?)?;        }         std::fs::create_dir_all(&self.run_directory)?;         Ok(())    }     pub fn worktree_path(&self) -> PathBuf {        self.worktrees_dir            .join(self.worktree_name())            .join(self.thread.meta().repo_name())    }     pub fn run(        &self,        model: Arc<dyn LanguageModel>,        app_state: Arc<AgentAppState>,        cx: &mut App,    ) -> Task<Result<RunOutput>> {        let project = Project::local(            app_state.client.clone(),            app_state.node_runtime.clone(),            app_state.user_store.clone(),            app_state.languages.clone(),            app_state.fs.clone(),            None,            cx,        );         let worktree = project.update(cx, |project, cx| {            project.create_worktree(self.worktree_path(), true, cx)        });         let tools = cx.new(|_| ToolWorkingSet::default());        let prompt_store = None;        let thread_store = ThreadStore::load(            project.clone(),            tools,            prompt_store,            app_state.prompt_builder.clone(),            cx,        );        let meta = self.thread.meta();        let this = self.clone();         cx.spawn(async move |cx| {            let worktree = worktree.await?;             // Wait for worktree scan to finish before choosing a file to open.            worktree                .update(cx, |worktree, _cx| {                    worktree.as_local().unwrap().scan_complete()                })?                .await;             struct LanguageServerState {                _lsp_open_handle: OpenLspBufferHandle,                language_file_buffer: Entity<Buffer>,            }             let mut diagnostics_before = None;            let mut diagnostic_summary_before = DiagnosticSummary::default();             let lsp = if let Some(language_server) = &meta.language_server {                // Open a file that matches the language to cause LSP to start.                let language_file = worktree.read_with(cx, |worktree, _cx| {                    worktree                        .files(false, 0)                        .find_map(|e| {                            if e.path.clone().extension().and_then(|ext| ext.to_str())                                == Some(&language_server.file_extension)                            {                                Some(ProjectPath {                                    worktree_id: worktree.id(),                                    path: e.path.clone(),                                })                            } else {                                None                            }                        })                        .context("Failed to find a file for example language")                })??;                 let open_language_file_buffer_task = project.update(cx, |project, cx| {                    project.open_buffer(language_file.clone(), cx)                })?;                 let language_file_buffer = open_language_file_buffer_task.await?;                 let lsp_open_handle = project.update(cx, |project, cx| {                    project.register_buffer_with_language_servers(&language_file_buffer, cx)                })?;                 wait_for_lang_server(&project, &language_file_buffer, this.log_prefix.clone(), cx).await?;                 diagnostic_summary_before = project.read_with(cx, |project, cx| {                      project.diagnostic_summary(false, cx)                })?;                 diagnostics_before = query_lsp_diagnostics(project.clone(), cx).await?;                if diagnostics_before.is_some() && language_server.allow_preexisting_diagnostics {                    anyhow::bail!("Example has pre-existing diagnostics. If you want to run this example regardless, set `allow_preexisting_diagnostics` to `true` in `base.toml`");                }                 Some(LanguageServerState {                    _lsp_open_handle: lsp_open_handle,                    language_file_buffer,                })            } else {                None            };             anyhow::ensure!(std::env::var("ZED_EVAL_SETUP_ONLY").is_err(), "Setup only mode");             let last_diff_file_path = this.run_directory.join("last.diff");             // Write an empty "last.diff" so that it can be opened in Zed for convenient view of the            // history using undo/redo.            std::fs::write(&last_diff_file_path, "")?;             let thread_store = thread_store.await?;              let thread =                thread_store.update(cx, |thread_store, cx| {                    let thread = if let Some(json) = &meta.existing_thread_json {                        let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread");                        thread_store.create_thread_from_serialized(serialized, cx)                    } else {                        thread_store.create_thread(cx)                    };                    thread.update(cx, |thread, cx| {                        thread.set_profile(meta.profile_id.clone(), cx);                    });                    thread                })?;              thread.update(cx, |thread, _cx| {                let mut request_count = 0;                let previous_diff = Rc::new(RefCell::new("".to_string()));                let example_output_dir = this.run_directory.clone();                let last_diff_file_path = last_diff_file_path.clone();                let messages_json_file_path = example_output_dir.join("last.messages.json");                let this = this.clone();                thread.set_request_callback(move |request, response_events| {                    request_count += 1;                    let messages_file_path = example_output_dir.join(format!("{request_count}.messages.md"));                    let diff_file_path = example_output_dir.join(format!("{request_count}.diff"));                    let last_messages_file_path = example_output_dir.join("last.messages.md");                    let request_markdown = RequestMarkdown::new(request);                    let response_events_markdown = response_events_to_markdown(response_events);                    let dialog = ThreadDialog::new(request, response_events);                    let dialog_json = serde_json::to_string_pretty(&dialog.to_combined_request()).unwrap_or_default();                     let messages = format!("{}\n\n{}", request_markdown.messages, response_events_markdown);                    fs::write(&messages_file_path, messages.clone()).expect("failed to write messages file");                    fs::write(&last_messages_file_path, messages).expect("failed to write last messages file");                    fs::write(&messages_json_file_path, dialog_json).expect("failed to write last.messages.json");                     let diff_result = smol::block_on(this.repository_diff());                    match diff_result {                        Ok(diff) => {                            if diff != previous_diff.borrow().clone() {                                fs::write(&diff_file_path, &diff).expect("failed to write diff file");                                fs::write(&last_diff_file_path, &diff).expect("failed to write last diff file");                                *previous_diff.borrow_mut() = diff;                            }                        }                        Err(err) => {                            let error_message = format!("{err:?}");                            fs::write(&diff_file_path, &error_message).expect("failed to write diff error to file");                            fs::write(&last_diff_file_path, &error_message).expect("failed to write last diff file");                        }                    }                     if request_count == 1 {                        let tools_file_path = example_output_dir.join("tools.md");                        fs::write(tools_file_path, request_markdown.tools).expect("failed to write tools file");                    }                });            })?;             let mut example_cx = ExampleContext::new(                meta.clone(),                this.log_prefix.clone(),                thread.clone(),                model.clone(),                cx.clone(),            );            let result = this.thread.conversation(&mut example_cx).await;             if let Err(err) = result {                if !err.is::<FailedAssertion>() {                    return Err(err);                }            }             println!("{}Stopped", this.log_prefix);             println!("{}Getting repository diff", this.log_prefix);            let repository_diff = this.repository_diff().await?;             std::fs::write(last_diff_file_path, &repository_diff)?;              let mut diagnostics_after = None;            let mut diagnostic_summary_after = Default::default();             if let Some(language_server_state) = lsp {                wait_for_lang_server(&project, &language_server_state.language_file_buffer, this.log_prefix.clone(), cx).await?;                 println!("{}Getting diagnostics", this.log_prefix);                diagnostics_after = cx                    .update(|cx| {                        let project = project.clone();                        cx.spawn(async move |cx| query_lsp_diagnostics(project, cx).await)                    })?                    .await?;                println!("{}Got diagnostics", this.log_prefix);                 diagnostic_summary_after = project.read_with(cx, |project, cx| {                      project.diagnostic_summary(false, cx)                })?;             }             if let Some(diagnostics_before) = &diagnostics_before {                fs::write(this.run_directory.join("diagnostics_before.txt"), diagnostics_before)?;            }             if let Some(diagnostics_after) = &diagnostics_after {                fs::write(this.run_directory.join("diagnostics_after.txt"), diagnostics_after)?;            }             thread.update(cx, |thread, _cx| {                let response_count = thread                    .messages()                    .filter(|message| message.role == language_model::Role::Assistant)                    .count();                RunOutput {                    repository_diff,                    diagnostic_summary_before,                    diagnostic_summary_after,                    diagnostics_before,                    diagnostics_after,                    response_count,                    token_usage: thread.cumulative_token_usage(),                    tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(),                    all_messages: messages_to_markdown(thread.messages()),                    programmatic_assertions: example_cx.assertions,                }            })        })    }     async fn repository_diff(&self) -> Result<String> {        let worktree_path = self.worktree_path();        run_git(&worktree_path, &["add", "."]).await?;        let mut diff_args = vec!["diff", "--staged"];        if self.thread.meta().url == ZED_REPO_URL {            diff_args.push(":(exclude).rules");        }        run_git(&worktree_path, &diff_args).await    }     pub async fn judge(        &self,        model: Arc<dyn LanguageModel>,        run_output: &RunOutput,        cx: &AsyncApp,    ) -> JudgeOutput {        let mut output_file =            File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md");         let diff_task = self.judge_diff(model.clone(), &run_output, cx);        let thread_task = self.judge_thread(model.clone(), &run_output, cx);         let (diff_result, thread_result) = futures::join!(diff_task, thread_task);         let (diff_response, diff_output) = diff_result;        let (thread_response, thread_output) = thread_result;         writeln!(            &mut output_file,            "# Judgment\n\n## Thread\n\n{thread_response}\n\n## Diff\n\n{diff_response}",        )        .log_err();         JudgeOutput {            thread: thread_output,            diff: diff_output,        }    }     async fn judge_diff(        &self,        model: Arc<dyn LanguageModel>,        run_output: &RunOutput,        cx: &AsyncApp,    ) -> (String, AssertionsReport) {        let diff_assertions = self.thread.diff_assertions();         if diff_assertions.is_empty() {            return (                "No diff assertions".to_string(),                AssertionsReport::default(),            );        }         println!("{}Running diff judge", self.log_prefix);         let judge_diff_prompt = include_str!("judge_diff_prompt.hbs");        let judge_diff_prompt_name = "judge_diff_prompt";

Lines 1–500 of 1216.