repositories / smith
smith
There are many coding harnesses - but this one is fast
owned by admin
smith-harness/src/runtime.rs
Raw//! Runtime assembly: configuration to agent (`SMH-SPEC-SPEC0001`,
//! Providers).
//!
//! One shape serves eval and RPC. Mock providers run scripted responses for
//! offline use; real providers post through the vendor transport with
//! credentials resolved from the environment.
use smith::config::Config;
use smith::error::Result;
use smith::provider::RecordSink;
use smith::provider::StreamFn;
use smith_ai::auth::AuthStore;
use smith_ai::models::ProviderKind;
use smith_ai::transport::{VendorEndpoint, transport};
use smith_core::agent::Agent;
use smith_core::store::open_session;
use smith_core::tools::ToolSession;
use std::path::{Path, PathBuf};
/// Which provider a runtime uses.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RuntimeProvider {
/// Scripted responses; `smh-mock` in the catalog.
Mock(Vec<Vec<smith::stream::StreamEvent>>),
/// OpenAI-compatible endpoint.
OpenAiCompatible {
/// Base URL, for example `https://api.openai.com/v1`.
base_url: String,
/// Model id.
model: String,
},
/// Anthropic Messages endpoint.
Anthropic {
/// Base URL.
base_url: String,
/// Model id.
model: String,
},
/// Google Gemini endpoint.
Google {
/// Base URL.
base_url: String,
/// Model id.
model: String,
},
}
impl RuntimeProvider {
/// The catalog model id this provider serves.
#[must_use]
pub fn model(&self) -> &str {
match self {
Self::Mock(_) => "mock-smith",
Self::OpenAiCompatible { model, .. }
| Self::Anthropic { model, .. }
| Self::Google { model, .. } => model,
}
}
const fn vendor_kind(&self) -> ProviderKind {
match self {
Self::Mock(_) | Self::OpenAiCompatible { .. } => ProviderKind::OpenAiCompatible,
Self::Anthropic { .. } => ProviderKind::Anthropic,
Self::Google { .. } => ProviderKind::Google,
}
}
}
/// Everything needed to assemble a runtime.
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
/// Provider selection.
pub provider: RuntimeProvider,
/// Working directory for tool effects.
pub workdir: PathBuf,
/// Session file path; created when missing.
pub session_path: PathBuf,
/// Explicit credential override; the highest-precedence source.
pub api_key: Option<String>,
/// Credential store directory; the binary passes the XDG default, tests
/// an isolated one so a developer's own credentials never leak in.
pub auth_dir: PathBuf,
}
impl RuntimeConfig {
/// A mock runtime under `workdir` with a fresh session file.
#[must_use]
pub fn mock(workdir: impl Into<PathBuf>, script: Vec<Vec<smith::stream::StreamEvent>>) -> Self {
let workdir = workdir.into();
let session_path = workdir.join("session.smh");
// An auth store beside the session keeps mock runtimes isolated.
let auth_dir = workdir.join("auth");
Self {
provider: RuntimeProvider::Mock(script),
workdir,
session_path,
api_key: None,
auth_dir,
}
}
}
impl RuntimeProvider {
/// A real provider endpoint with an explicit model; the base URL falls
/// back to the vendor default.
#[must_use]
pub fn real(kind: ProviderKind, base_url: Option<String>, model: String) -> Self {
let base_url =
base_url.unwrap_or_else(|| smith_ai::auth::default_base_url(kind).to_string());
match kind {
ProviderKind::OpenAiCompatible | ProviderKind::Plugin => {
Self::OpenAiCompatible { base_url, model }
}
ProviderKind::Anthropic => Self::Anthropic { base_url, model },
ProviderKind::Google => Self::Google { base_url, model },
}
}
}
/// Assemble an agent from a configuration, opening or creating its session.
///
/// # Errors
///
/// Returns configuration, session, or credential-resolution failures; a
/// missing credential for a real provider fails here, before any dispatch.
pub fn build_agent(config: &RuntimeConfig) -> Result<Agent> {
let smith_config = Config::default_valid();
let opened = open_session(Path::new(&config.session_path), smith_config)?;
let tools = ToolSession::with_session(config.workdir.clone(), opened.session, opened.writer);
let (sink, drain) = smith::provider::record_channel();
let stream = resolve_stream(
&config.provider,
config.api_key.clone(),
&AuthStore::at(&config.auth_dir),
sink,
)?;
Ok(Agent::with_provider_records(
stream,
tools,
config.provider.model().to_string(),
Some(drain),
))
}
fn resolve_stream(
provider: &RuntimeProvider,
api_key: Option<String>,
store: &AuthStore,
records: RecordSink,
) -> Result<StreamFn> {
match provider {
RuntimeProvider::Mock(script) => Ok(crate::mock_stream(script.clone())),
RuntimeProvider::OpenAiCompatible { base_url, model }
| RuntimeProvider::Anthropic { base_url, model }
| RuntimeProvider::Google { base_url, model } => {
let kind = provider.vendor_kind();
let api_key = match api_key {
Some(explicit) => explicit,
None => resolve_credential(store, kind)?,
};
Ok(transport(VendorEndpoint {
kind,
base_url: base_url.clone(),
model: model.clone(),
api_key,
executor: crate::http::default_executor(),
records,
}))
}
}
}
/// Credentials resolve `store` first, then the provider environment
/// variable; a miss names both sources and fails before dispatch.
///
/// # Errors
///
/// Returns an authentication fault naming the provider and every source
/// tried, without network access.
fn resolve_credential(store: &AuthStore, kind: ProviderKind) -> Result<String> {
match store.load(kind) {
Ok(Some(secret)) => Ok(secret),
Ok(None) => {
let var = smith_ai::auth::credential_env_var(kind);
match std::env::var(var) {
Ok(value) if !value.is_empty() => Ok(value),
_ => Err(smith::error::SmithError::Provider {
fault: smith::error::ProviderFault::Authentication {
message: format!(
"no credential for {} (tried store, env {var})",
kind.slug()
),
},
}),
}
}
Err(error) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock_text_reply;
#[test]
fn mock_runtime_completes_and_persists_a_turn() {
let dir = std::env::temp_dir().join(format!("smith_harness_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap_or(());
let config = RuntimeConfig::mock(dir.join("work"), vec![mock_text_reply("assembled")]);
let mut agent = build_agent(&config).unwrap();
let outcome = agent.run_turn("hello").unwrap();
assert_eq!(outcome.text, "assembled");
drop(agent);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn real_providers_fail_fast_without_credentials() {
let dir = std::env::temp_dir().join(format!("smith_harness_auth_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap_or(());
// The credential check reads the environment; a missing variable
// is the normal fresh-runner state, so the test does not mutate it.
let config = RuntimeConfig {
provider: RuntimeProvider::OpenAiCompatible {
base_url: "https://example.test/v1".to_string(),
model: "gpt-5.1".to_string(),
},
api_key: None,
..RuntimeConfig::mock(&dir, Vec::new())
};
let Err(err) = build_agent(&config) else {
unreachable!("no credential must fail assembly");
};
assert_eq!(err.code(), "PROVIDER_AUTH");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn stored_credential_resolves_without_network() {
let home = std::env::temp_dir().join(format!("smith_harness_store_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&home);
let store = AuthStore::at(&home);
store
.save(smith_ai::models::ProviderKind::Anthropic, "sk-from-store")
.unwrap_or(());
assert_eq!(
resolve_credential(&store, smith_ai::models::ProviderKind::Anthropic)
.unwrap_or_default(),
"sk-from-store"
);
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn real_provider_defaults_base_url_per_vendor() {
let anthropic = RuntimeProvider::real(
smith_ai::models::ProviderKind::Anthropic,
None,
"m".to_string(),
);
let RuntimeProvider::Anthropic { base_url, model } = anthropic else {
unreachable!("anthropic kind builds the anthropic endpoint");
};
assert_eq!(base_url, "https://api.anthropic.com");
assert_eq!(model, "m");
let overridden = RuntimeProvider::real(
smith_ai::models::ProviderKind::Google,
Some("https://proxy.test".to_string()),
"m".to_string(),
);
let RuntimeProvider::Google { base_url, .. } = overridden else {
unreachable!("google kind builds the google endpoint");
};
assert_eq!(base_url, "https://proxy.test");
}
}