use super::{
Deps, Error, GLOBAL_MODELS_FILE, PER_REPO_PROVIDERS_FILE, SOULS_DIR, WORKER_ROLE, WORKFLOW_FILE,
};
use crate::config::manifest::{Manifest, RoleRules};
use crate::config::version::Version;
use crate::config::{ModelsConfig, Workflow, cross};
use crate::prompt::{AdapterRunner, adapter, brazen_pin, dispatch};
use crate::workspace;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const VERSION_FILE: &str = "version";
const MANIFEST_FILE: &str = "manifest.yaml";
pub(super) enum ConfigSource<'a> {
Fork(&'a str),
Agent(&'a str),
}
#[derive(Clone)]
pub(super) struct WorkerConfig {
pub(super) role: String,
pub(super) model_id: String,
pub(super) provider_row: String,
pub(super) tools: Vec<String>,
pub(super) config_commit: String,
pub(super) soul: String,
pub(super) binary: OsString,
pub(super) workflow: Workflow,
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,
soul: self.soul.clone(),
binary: self.binary.clone(),
retry: self.workflow.retry,
budgets: self.workflow.budgets,
workflow: &self.workflow,
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 {
check_bz_version(deps.adapter, &binary)?;
}
let workflow_raw = read_control(workspace, &commit, WORKFLOW_FILE, deps)?;
let workflow = Workflow::parse(&workflow_raw, &control_origin(&commit, WORKFLOW_FILE))?;
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(),
tools: assignment.tools.clone(),
config_commit: commit,
soul,
binary,
workflow,
manifest,
expect_handshake,
})
}
fn agent_role(
workspace: &Path,
source: &ConfigSource<'_>,
deps: &Deps<'_>,
) -> Result<String, Error> {
match source {
ConfigSource::Fork(_) => Ok(WORKER_ROLE.to_string()),
ConfigSource::Agent(agent_id) => Ok(crate::prompt::role::derive(
&workspace::repo_git(workspace),
&workspace::agent_ref(agent_id),
agent_id,
deps.git,
)?
.unwrap_or_else(|| WORKER_ROLE.to_string())),
}
}
fn config_commit(
workspace: &Path,
source: &ConfigSource<'_>,
deps: &Deps<'_>,
) -> Result<String, Error> {
let rev = match source {
ConfigSource::Fork(fork_point) => (*fork_point).to_owned(),
ConfigSource::Agent(agent_id) => workspace::agent_ref(agent_id),
};
workspace::governing_config(workspace, &rev, deps.git).map_err(|source| Error::Git {
op: "governing config",
source,
})
}
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}"))
}
fn check_bz_version(adapter: &dyn AdapterRunner, binary: &OsString) -> Result<(), Error> {
let out = adapter::capture_stdout(adapter, binary, &["--version"])
.map_err(|e| adapter::spawn_error(binary, e))?;
let found = out.split_whitespace().last().unwrap_or("").to_string();
if found != brazen_pin() {
return Err(Error::VersionSkew {
found,
expected: brazen_pin().to_string(),
});
}
Ok(())
}