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 .map(|backend| serde_json::to_value(backend)
83 .expect("backend is JSON")
84 .as_str()
85 .expect("backend is text")
86 .to_owned())
87 .unwrap_or_else(|| "?".into()),
88 header.model.as_deref().unwrap_or("?"),
89 header.reasoning.map_or(String::new(), |reasoning| format!(
90 " (reasoning {reasoning})"
91 ))
92 ),
93 files_changed,
94 };
95 let mut recorder = Recorder::append(state, &conversation.path, &located.id, sent.number)?;
96 let answer = agent::follow_up(
97 &config,
98 eval,
99 agent::Continuation {
100 conversation: &conversation,
101 files_changed,
102 output: &output,
103 state,
104 },
105 message,
106 &mut recorder,
107 cancellation,
108 )
109 .await;
110 Ok((sent, answer))
111}
112
113async fn files_changed(
116 config: &crate::config::RepoConfig,
117 request: &store::Request,
118 output: &Path,
119 cancellation: tokio_util::sync::CancellationToken,
120) -> Option<bool> {
121 if request.fingerprints.is_empty() {
122 return None;
123 }
124 if request
125 .fingerprints
126 .keys()
127 .any(|id| !config.artifacts.contains_key(id))
128 {
129 return Some(true);
130 }
131 let current = cache::prepare(
132 config,
133 request.fingerprints.keys(),
134 output,
135 &cache::Parallelism::new(cache::Parallelism::available()),
136 cancellation,
137 )
138 .await
139 .ok()?;
140 Some(
141 request
142 .fingerprints
143 .iter()
144 .any(|(id, fingerprint)| current.get(id).is_none_or(|now| &now.value != fingerprint)),
145 )
146}