mod source;
pub(crate) mod workflow_source;
pub(crate) use source::ConfigSource;
use source::{agent_role, config_commit};
#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_assignment;
#[cfg(test)]
mod tests_declines;
use super::{Deps, Error, GLOBAL_MODELS_FILE, PER_REPO_PROVIDERS_FILE, SOULS_DIR};
use crate::config::manifest::{Manifest, RoleRules};
use crate::config::version::Version;
use crate::config::{ModelsConfig, Workflow, cross};
use crate::prompt::{adapter, dispatch};
use crate::workspace;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const VERSION_FILE: &str = "version";
const MANIFEST_FILE: &str = "manifest.yaml";
#[derive(Clone)]
pub(super) struct WorkerConfig {
pub(super) role: String,
pub(super) model_id: String,
pub(super) provider_row: String,
pub(super) effort: Option<crate::config::Effort>,
pub(super) priority: Option<bool>,
pub(super) max_output_tokens: Option<u32>,
pub(super) tools: Vec<String>,
pub(super) config_commit: String,
pub(super) soul: String,
pub(super) binary: OsString,
pub(super) workflow: Workflow,
pub(super) workflow_commit: String,
pub(super) manifest: Option<RoleRules>,
pub(super) expect_handshake: bool,
}
impl WorkerConfig {
pub(super) fn as_resolved(&self) -> dispatch::Resolved<'_> {
dispatch::Resolved {
grant: dispatch::Grant {
role: &self.role,
tools: &self.tools,
config_commit: &self.config_commit,
},
model_id: &self.model_id,
provider_row: &self.provider_row,
effort: self.effort,
priority: self.priority,
max_output_tokens: self.max_output_tokens,
soul: self.soul.clone(),
binary: self.binary.clone(),
retry: self.workflow.retry,
budgets: self.workflow.budgets,
workflow: &self.workflow,
workflow_commit: &self.workflow_commit,
manifest: self.manifest.as_ref(),
expect_handshake: self.expect_handshake,
}
}
}
pub(super) fn resolve_worker(
workspace: &Path,
source: ConfigSource<'_>,
deps: &Deps<'_>,
) -> Result<WorkerConfig, Error> {
let commit = config_commit(workspace, &source, deps)?;
let role = agent_role(workspace, &source, deps)?;
let version_raw = read_control(workspace, &commit, VERSION_FILE, deps)?;
Version::parse(&version_raw, &control_origin(&commit, VERSION_FILE))?;
let global_path = deps.config_root.join(GLOBAL_MODELS_FILE);
let providers_raw = read_control(workspace, &commit, PER_REPO_PROVIDERS_FILE, deps)?;
let cfg = ModelsConfig::load_with_per_repo(
&global_path,
&providers_raw,
&control_origin(&commit, PER_REPO_PROVIDERS_FILE),
)?;
let assignment = cfg
.per_repo
.roles
.get(role.as_str())
.ok_or_else(|| Error::RoleMissing(role.clone()))?;
let adapter_override = cfg.global.adapter.as_deref();
let host = deps.adapter_target;
let binary = adapter::resolve_binary(adapter_override, host);
let expect_handshake = adapter_override.is_some() || host.is_some();
if !expect_handshake {
adapter::check_bz_version(deps.adapter, &binary)?;
}
let (workflow, workflow_commit) =
workflow_source::resolve_workflow(workspace, &source, &commit, deps)?;
cross::check_workflow_against_roles(&workflow, &cfg.per_repo)?;
let manifest_raw = read_control(workspace, &commit, MANIFEST_FILE, deps)?;
let manifest = Manifest::parse(&manifest_raw, &control_origin(&commit, MANIFEST_FILE))?
.roles
.remove(role.as_str());
let soul_rel = format!("{SOULS_DIR}/{role}.md");
let soul = read_control(workspace, &commit, &soul_rel, deps)?;
Ok(WorkerConfig {
role,
model_id: assignment.model.clone(),
provider_row: assignment.provider.clone(),
effort: assignment.effort,
priority: assignment.priority,
max_output_tokens: assignment.max_output_tokens,
tools: assignment.tools.clone(),
config_commit: commit,
soul,
binary,
workflow,
workflow_commit,
manifest,
expect_handshake,
})
}
fn read_control(
workspace: &Path,
commit: &str,
path: &str,
deps: &Deps<'_>,
) -> Result<String, Error> {
workspace::show_control(workspace, commit, path, deps.git).map_err(|source| {
Error::ControlRead {
path: control_origin(commit, path),
source,
}
})
}
fn control_origin(commit: &str, path: &str) -> PathBuf {
PathBuf::from(format!("{commit}:{path}"))
}