artifactize_core/inspect/session/
send.rs1use super::Located;
3use crate::{
4 agent::{
5 self,
6 session::{self, Recorder},
7 },
8 cache,
9 config::read_workspace_config,
10 store, workspace,
11};
12use std::path::Path;
13
14pub struct Sent {
16 pub number: usize,
17 pub model: String,
18 pub files_changed: Option<bool>,
19}
20
21pub async fn send(
23 state: &Path,
24 repo: Option<&Path>,
25 located: &Located,
26 message: &str,
27 cancellation: tokio_util::sync::CancellationToken,
28) -> Result<(Sent, agent::FollowUp), String> {
29 if matches!(
30 located.request.request.status(),
31 crate::types::RequestStatus::Queued | crate::types::RequestStatus::Running
32 ) {
33 return Err(format!(
34 "Request {} is still running; send a follow-up once its review has finished.",
35 located.request.request.id
36 ));
37 }
38 located.load()?;
40 let _lock = tokio::select! {
41 lock = session::lock(state, &located.id) => lock?,
42 () = cancellation.cancelled() => {
43 return Err("Cancelled while waiting for another send to this session.".into());
44 }
45 };
46 let conversation = located.load()?;
47 let request = &located.request.request;
48 let repo = match repo {
49 Some(repo) => repo.to_path_buf(),
50 None => store::read_run(state, &request.run_id).await?.run.repo_path,
51 };
52 let config = read_workspace_config(&repo).map_err(|error| {
53 format!(
54 "Cannot resolve the review's tools in {}: {error}",
55 crate::platform::path_text(&repo)
56 )
57 })?;
58 let eval = config
59 .evals
60 .iter()
61 .find(|eval| eval.id == request.eval_id)
62 .ok_or_else(|| {
63 format!(
64 "Eval {} is no longer declared in {}; its tools cannot be resolved.",
65 request.eval_id,
66 crate::platform::path_text(&repo)
67 )
68 })?;
69 let output = workspace::prepare_directory(
70 &state.join("runs").join(request.run_id.as_str()),
71 &config.root,
72 )
73 .map_err(|e| e.to_string())?;
74 let files_changed = files_changed(&config, request, &output, cancellation.clone()).await;
75 let header = conversation.header();
76 let sent = Sent {
77 number: conversation.sends() + 1,
78 model: format!(
79 "{} {}{}",
80 header
81 .backend
82 .as_ref()
83 .map_or("?", crate::config::Backend::as_str),
84 header.model.as_deref().unwrap_or("?"),
85 header.reasoning.map_or(String::new(), |reasoning| format!(
86 " (reasoning {reasoning})"
87 ))
88 ),
89 files_changed,
90 };
91 let mut recorder = Recorder::append(state, &conversation.path, &located.id, sent.number)?;
92 let answer = agent::follow_up(
93 &config,
94 eval,
95 agent::Continuation {
96 conversation: &conversation,
97 files_changed,
98 output: &output,
99 state,
100 },
101 message,
102 &mut recorder,
103 cancellation,
104 )
105 .await;
106 Ok((sent, answer))
107}
108
109async fn files_changed(
112 config: &crate::config::RepoConfig,
113 request: &store::Request,
114 output: &Path,
115 cancellation: tokio_util::sync::CancellationToken,
116) -> Option<bool> {
117 if request.fingerprints.is_empty() {
118 return None;
119 }
120 if request
121 .fingerprints
122 .keys()
123 .any(|id| !config.artifacts.contains_key(id))
124 {
125 return Some(true);
126 }
127 let current = cache::prepare(
128 config,
129 request.fingerprints.keys(),
130 output,
131 &cache::Parallelism::new(cache::Parallelism::available()),
132 cancellation,
133 )
134 .await
135 .ok()?;
136 Some(
137 request
138 .fingerprints
139 .iter()
140 .any(|(id, fingerprint)| current.get(id).is_none_or(|now| &now.value != fingerprint)),
141 )
142}