1446 lines · 50.8 KB · Rust
RawDownload
use agent::ContextServerRegistry;use agent_client_protocol as acp;use anyhow::{Context as _, Result, anyhow, bail};use client::proto::LspWorkProgress;use futures::channel::mpsc;use futures::future::Shared;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, LanguageModelRegistry, LanguageModelRequest,    LanguageModelRequestMessage, LanguageModelToolResultContent, MessageContent, Role, TokenUsage,};use project::{DiagnosticSummary, Project, ProjectPath, lsp_store::OpenLspBufferHandle};use prompt_store::{ProjectContext, WorktreeContext};use rand::{distr, prelude::*};use serde::{Deserialize, Serialize};use std::{    fmt::Write as _,    fs::{self, File},    io::Write as _,    path::{Path, PathBuf},    rc::Rc,    sync::{Arc, Mutex},    time::Duration,};use unindent::Unindent as _;use util::{ResultExt as _, command::new_command, markdown::MarkdownCodeBlock}; use crate::{    AgentAppState, ToolMetrics,    assertions::{AssertionsReport, RanAssertion, RanAssertionResult},    example::{Example, ExampleContext, FailedAssertion, JudgeAssertion},}; 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 token_usage: TokenUsage,    pub tool_metrics: ToolMetrics,    pub thread_markdown: 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;        let run_directory = run_dir.join(&name).join(repetition.to_string());         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().into_owned();             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, 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,            project::LocalProjectFlags {                init_worktree_trust: false,                ..Default::default()            },            cx,        );         let worktree = project.update(cx, |project, cx| {            project.create_worktree(self.worktree_path(), true, 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()                                    == 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 = cx.update(|cx| {                //todo: Do we want to load rules files here?                let worktrees = project.read(cx).visible_worktrees(cx).map(|worktree| {                    let root_name = worktree.read(cx).root_name_str().into();                    let abs_path = worktree.read(cx).abs_path();                     WorktreeContext {                        root_name,                        abs_path,                        rules_file: None,                    }                }).collect::<Vec<_>>();                let project_context = cx.new(|_cx| ProjectContext::new(worktrees, vec![]));                let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));                 let thread = if let Some(json) = &meta.existing_thread_json {                    let session_id = acp::SessionId::new(                        rand::rng()                            .sample_iter(&distr::Alphanumeric)                            .take(7)                            .map(char::from)                            .collect::<String>(),                    );                     let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread");                    cx.new(|cx| agent::Thread::from_db(session_id, db_thread, project.clone(), project_context, context_server_registry, agent::Templates::new(), cx))                } else {                    cx.new(|cx| agent::Thread::new(project.clone(), project_context, context_server_registry, agent::Templates::new(), None, cx))                };                 thread.update(cx, |thread, cx| {                    thread.add_default_tools(Rc::new(EvalThreadEnvironment {                        project: project.clone(),                    }), cx);                    thread.set_profile(meta.profile_id.clone(), cx);                    thread.set_model(                        LanguageModelInterceptor::new(                            LanguageModelRegistry::read_global(cx).default_model().expect("Missing model").model.clone(),                            this.run_directory.clone(),                            last_diff_file_path.clone(),                            this.run_directory.join("last.messages.json"),                            this.worktree_path(),                            this.repo_url(),                        ),                        cx,                    );                });                 thread            });             let mut example_cx = ExampleContext::new(                meta.clone(),                this.log_prefix.clone(),                thread.clone(),                cx.clone(),            );            let result = this.thread.conversation(&mut example_cx).await;             if let Err(err) = result                && !err.is::<FailedAssertion>() {                    return Err(err);                }             println!("{}Stopped", this.log_prefix);             println!("{}Getting repository diff", this.log_prefix);            let repository_diff = Self::repository_diff(this.worktree_path(), &this.repo_url()).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)?;            }             Ok(thread.update(cx, |thread, _cx| {                RunOutput {                    repository_diff,                    diagnostic_summary_before,                    diagnostic_summary_after,                    diagnostics_before,                    diagnostics_after,                    token_usage: thread.latest_request_token_usage().unwrap(),                    tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(),                    thread_markdown: thread.to_markdown(),                    programmatic_assertions: example_cx.assertions,                }            }))        })    }     async fn repository_diff(repository_path: PathBuf, repository_url: &str) -> Result<String> {        run_git(&repository_path, &["add", "."]).await?;        let mut diff_args = vec!["diff", "--staged"];        if repository_url == ZED_REPO_URL {            diff_args.push(":(exclude).rules");        }        run_git(&repository_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";        let mut hbs = Handlebars::new();        hbs.register_template_string(judge_diff_prompt_name, judge_diff_prompt)            .unwrap();         let to_prompt = |assertion: String| {            hbs.render(                judge_diff_prompt_name,                &JudgeDiffInput {                    repository_diff: run_output.repository_diff.clone(),                    assertion,                },            )            .unwrap()        };         let (responses, report) = self            .judge_assertions(model, diff_assertions, to_prompt, cx)            .await;         println!(            "{}Judge - Diff score: {}%",            self.log_prefix,            report.passed_percentage()        );         (responses, report)    }     async fn judge_thread(        &self,        model: Arc<dyn LanguageModel>,        run_output: &RunOutput,

Lines 1–500 of 1446.