use std::sync::Arc;
use everruns::LlmSimConfig;
#[derive(Clone, Debug)]
pub struct Markdown {
path: &'static str,
embedded: &'static str,
disk: &'static str,
}
impl Markdown {
#[doc(hidden)]
pub const fn __embedded(
path: &'static str,
embedded: &'static str,
disk: &'static str,
) -> Self {
Self {
path,
embedded,
disk,
}
}
pub fn path(&self) -> &'static str {
self.path
}
pub fn load(&self, hot_reload: bool) -> String {
if hot_reload && let Ok(text) = std::fs::read_to_string(self.disk) {
return text;
}
self.embedded.to_string()
}
}
#[derive(Clone, Debug)]
pub enum Instructions {
Text(String),
Markdown(Markdown),
}
impl Instructions {
pub(crate) fn load(&self, hot_reload: bool) -> String {
match self {
Instructions::Text(text) => text.clone(),
Instructions::Markdown(markdown) => markdown.load(hot_reload),
}
}
}
impl From<&str> for Instructions {
fn from(text: &str) -> Self {
Instructions::Text(text.to_string())
}
}
impl From<String> for Instructions {
fn from(text: String) -> Self {
Instructions::Text(text)
}
}
impl From<Markdown> for Instructions {
fn from(markdown: Markdown) -> Self {
Instructions::Markdown(markdown)
}
}
type Customize = Arc<dyn Fn(everruns::AgentBuilder) -> everruns::AgentBuilder + Send + Sync>;
#[derive(Clone)]
pub struct Agent {
pub(crate) model: String,
pub(crate) instructions: Option<Instructions>,
pub(crate) description: Option<String>,
pub(crate) tools: Option<Vec<String>>,
pub(crate) offline: Option<LlmSimConfig>,
pub(crate) customize: Option<Customize>,
pub(crate) package: Option<everruns::AgentPackage>,
}
impl std::fmt::Debug for Agent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Agent")
.field("model", &self.model)
.field("instructions", &self.instructions)
.field("tools", &self.tools)
.finish_non_exhaustive()
}
}
impl Agent {
pub fn from_package(path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
Self::package(everruns::AgentPackage::load(path)?)
}
pub fn package(package: everruns::AgentPackage) -> crate::Result<Self> {
let m = package.manifest();
if m.sandbox_policy.is_some() {
anyhow::bail!("package sandbox policy requires an explicit host binding");
}
if let Some(harness) = m.harness.as_deref()
&& !matches!(harness, "base" | "conversation" | "worker-base" | "worker")
{
anyhow::bail!(
"package harness {harness} requires an explicit Framework session binding"
);
}
let model = m
.model
.as_ref()
.map(|m| {
if m.provider.as_str() == "llmsim" {
"sim".into()
} else {
format!("{}/{}", m.provider, m.model)
}
})
.unwrap_or_else(|| "sim".into());
let mut spec = Self::builder()
.model(model)
.instructions(m.instructions.clone())
.build();
spec.description = m.description.clone();
spec.tools = Some(m.tools.iter().map(|t| t.name().into()).collect());
spec.package = Some(package);
Ok(spec)
}
pub fn builder() -> AgentBuilder {
AgentBuilder {
agent: Agent {
model: String::new(),
instructions: None,
description: None,
tools: None,
offline: None,
customize: None,
package: None,
},
}
}
pub fn model(&self) -> &str {
&self.model
}
}
#[must_use]
pub struct AgentBuilder {
agent: Agent,
}
impl AgentBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.agent.model = model.into();
self
}
pub fn instructions(mut self, instructions: impl Into<Instructions>) -> Self {
self.agent.instructions = Some(instructions.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.agent.description = Some(description.into());
self
}
pub fn tools<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.agent.tools = Some(names.into_iter().map(Into::into).collect());
self
}
pub fn offline(mut self, script: LlmSimConfig) -> Self {
self.agent.offline = Some(script);
self
}
pub fn customize(
mut self,
f: impl Fn(everruns::AgentBuilder) -> everruns::AgentBuilder + Send + Sync + 'static,
) -> Self {
self.agent.customize = Some(Arc::new(f));
self
}
pub fn build(self) -> Agent {
self.agent
}
}