use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use tokio_util::sync::CancellationToken;
use crate::{
agent::verdict::validate_result,
cache,
config::{Eval, RepoConfig, read_workspace_config},
remote::Session,
scope,
store::{self, HumanClaim, Receipts, Request},
tools::human::{CommandLine, Registry, ToolResult},
};
pub const MAX_RESULT_BYTES: usize = 256_000;
pub const FIELDS_READ_BYTES: u64 = MAX_RESULT_BYTES as u64 + 1;
pub fn default_reviewer() -> Result<crate::types::ReviewerId, String> {
crate::platform::USER_VARIABLES
.iter()
.find_map(|name| crate::platform::environment::var_text(name))
.ok_or("Provide an explicit reviewer id; no default reviewer is available.")?
.parse()
}
pub async fn open(
state: &Path,
request: &crate::types::RequestId,
) -> Result<(Receipts, PathBuf), String> {
let (_, repo) = store::read_request_repo(state, request).await?;
Ok((Receipts::open(state, &repo).await?, repo))
}
pub async fn claim(
receipts: &Receipts,
request: &crate::types::RequestId,
reviewer: &crate::types::ReviewerId,
) -> Result<HumanClaim, String> {
receipts.claim_human(request, reviewer).await
}
pub async fn unclaim(
receipts: &Receipts,
request: &crate::types::RequestId,
reviewer: &crate::types::ReviewerId,
) -> Result<HumanClaim, String> {
receipts.release_human(request, reviewer).await
}
pub async fn run_human_tool(
receipts: &Receipts,
request: &crate::types::RequestId,
reviewer: &crate::types::ReviewerId,
tool: &crate::config::ToolName,
cancellation: CancellationToken,
run: crate::tools::human::log::Run,
) -> Result<ToolResult, String> {
let ready = async {
let (mut request, _) = receipts.human_request(request, reviewer).await?;
let config = reconnect(&request)?;
let registry = Registry::new(&config, &request.eval_id)?;
if !registry.list().any(|entry| &entry.name == tool) {
return Err::<_, String>("Unknown registered Human tool.".into());
}
recheck(
receipts,
&mut request,
reviewer,
&config,
cancellation.clone(),
)
.await?;
receipts.human_request(&request.id, reviewer).await?;
Ok((config, request.eval_id))
};
let (config, eval) = match ready.await {
Ok(ready) => ready,
Err(error) => {
run.end(crate::tools::human::log::End::Failed(error.clone()));
return Err(error);
}
};
let registry = Registry::new(&config, &eval).expect("the registry was just built");
Ok(registry.call_logged(tool, cancellation, run).await)
}
pub async fn tool_commands(
receipts: &Receipts,
request: &crate::types::RequestId,
) -> Result<std::collections::BTreeMap<crate::config::ToolName, Result<CommandLine, String>>, String>
{
let request = receipts.waiting_human(request).await?;
let config = reconnect(&request)?;
let registry = Registry::new(&config, &request.eval_id)?;
Ok(registry
.list()
.filter(|tool| registry.is_command(&tool.name))
.map(|tool| (tool.name.clone(), registry.command(&tool.name)))
.collect())
}
pub async fn submit(
receipts: &Receipts,
request: &crate::types::RequestId,
reviewer: &crate::types::ReviewerId,
result: &Value,
cancellation: CancellationToken,
) -> Result<Request, String> {
let (mut request, _) = receipts.human_request(request, reviewer).await?;
let config = reconnect(&request)?;
let eval = config
.evals
.iter()
.find(|eval| eval.id == request.eval_id)
.expect("reconnected eval");
if serde_json::to_vec(result).map_err(|e| e.to_string())?.len() > MAX_RESULT_BYTES {
return Err(format!("Human result exceeds {MAX_RESULT_BYTES} bytes."));
}
let result = validate_result(&eval.declaration, result)?;
recheck(receipts, &mut request, reviewer, &config, cancellation).await?;
request.state = crate::store::RequestState::completed(result.into(), crate::broker::now());
receipts.settle_human(&request, reviewer).await
}
pub async fn submit_and_publish(
state: &Path,
request: &crate::types::RequestId,
reviewer: &crate::types::ReviewerId,
result: &Value,
cancellation: CancellationToken,
) -> Result<Request, String> {
let (receipts, repo) = open(state, request).await?;
let result = async {
let remote = Session::open(Some(state), Some(&repo))?;
let request = submit(&receipts, request, reviewer, result, cancellation).await?;
if let Some(remote) = remote {
remote
.publish_request(&receipts, &request)
.await
.map_err(|error| {
format!("The Human result is saved locally, but publishing it failed: {error}")
})?;
}
Ok(request)
}
.await;
let closed = receipts.close().await;
result.and_then(|request| closed.map(|()| request))
}
pub(crate) fn definition(
config: &RepoConfig,
eval: &Eval,
) -> Result<crate::store::HumanDefinition, String> {
let scope = scope::eval_scope(config, eval).map_err(|e| e.to_string())?;
serde_json::from_value(json!({"repo":crate::platform::path_text(&config.root),"eval":eval,"artifacts":scope.artifacts})).map_err(|e| e.to_string())
}
fn reconnect(request: &Request) -> Result<RepoConfig, String> {
let repo = &request
.provenance
.as_ref()
.ok_or("Human request has no provenance.")?
.repo_path;
let config = read_workspace_config(repo).map_err(|e| e.to_string())?;
let eval = config
.evals
.iter()
.find(|eval| eval.id == request.eval_id)
.ok_or("Recorded Human eval no longer exists.")?;
if Some(definition(&config, eval)?) != request.human_definition {
return Err(
"Human Artifact scope or eval declarations changed since the request was recorded."
.into(),
);
}
Ok(config)
}
async fn recheck(
receipts: &Receipts,
request: &mut Request,
reviewer: &crate::types::ReviewerId,
config: &RepoConfig,
cancellation: CancellationToken,
) -> Result<(), String> {
if cancellation.is_cancelled() {
return Err("Human action was cancelled.".into());
}
let Some(expected) = &request.key else {
return Ok(());
};
let output = request
.run_dir
.as_deref()
.ok_or("Human request has no output directory.")?;
let eval = config
.evals
.iter()
.find(|eval| eval.id == request.eval_id)
.ok_or("Recorded Human eval no longer exists.")?;
let parallelism = cache::Parallelism::new(cache::Parallelism::available());
let (code, error) =
match cache::recheck(config, eval, output, ¶llelism, cancellation.clone()).await {
Ok(Some(actual)) if &actual == expected => return Ok(()),
Ok(_) => (
crate::types::FailureCode::InputChanged,
"Fingerprint changed during review.".into(),
),
Err(error) if cancellation.is_cancelled() => return Err(error),
Err(error) => (crate::types::FailureCode::FingerprintRecheckFailed, error),
};
request.state =
crate::store::RequestState::failed(error.clone(), Some(code), crate::broker::now());
receipts.settle_human(request, reviewer).await?;
Err(error)
}