use std::future::Future;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use autumn_web::app::AppBuilder;
use autumn_web::plugin::Plugin;
use autumn_web::reexports::axum::extract::FromRequestParts;
use autumn_web::{AppState, AutumnError};
use http::request::Parts;
use crate::agent::{Agent, AgentOutcome, AgentRuntime, AgentTurn, ApprovalDecision, RunState};
use crate::client::client_from_config;
use crate::config::AgentConfig;
use crate::error::AgentError;
use crate::health::AgentHealthIndicator;
use crate::hooks::AgentHooks;
use crate::ids::SessionId;
use crate::memory::{MemoryScope, MemoryStore};
use crate::policy::ToolPolicy;
use crate::proactive::{Delivery, Heartbeat, HeartbeatSettings};
use crate::session::{Compaction, SessionStore};
use crate::skills::Skill;
use crate::tools::Tool;
pub struct AgentPlugin {
config: AgentConfig,
tools: Vec<Arc<dyn Tool>>,
hooks: Vec<Arc<dyn AgentHooks>>,
policy: Option<Arc<dyn ToolPolicy>>,
sessions: Option<Arc<dyn SessionStore>>,
memory: Option<Arc<dyn MemoryStore>>,
skills: Vec<Skill>,
compaction: Option<Compaction>,
delivery: Option<Arc<dyn Delivery>>,
heartbeat: Option<Heartbeat>,
followups: Option<Duration>,
}
impl std::fmt::Debug for AgentPlugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentPlugin")
.field("config", &self.config)
.field(
"tools",
&self
.tools
.iter()
.map(|tool| tool.name().to_owned())
.collect::<Vec<_>>(),
)
.field("hooks", &self.hooks.len())
.field("policy", &self.policy)
.field("memory", &self.memory.is_some())
.field(
"skills",
&self
.skills
.iter()
.map(|skill| skill.name.as_str())
.collect::<Vec<_>>(),
)
.field("compaction", &self.compaction)
.field("heartbeat", &self.heartbeat)
.field("followups", &self.followups)
.finish_non_exhaustive()
}
}
impl AgentPlugin {
#[must_use]
pub fn new() -> Self {
Self {
config: AgentConfig::default(),
tools: Vec::new(),
hooks: Vec::new(),
policy: None,
sessions: None,
memory: None,
skills: Vec::new(),
compaction: None,
delivery: None,
heartbeat: None,
followups: None,
}
}
#[must_use]
pub fn configure(mut self, configure: impl FnOnce(&mut AgentConfig)) -> Self {
configure(&mut self.config);
self
}
#[must_use]
pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
self.tools.push(tool);
self
}
#[must_use]
pub fn tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
self.tools.extend(tools);
self
}
#[must_use]
pub fn hook(mut self, hook: Arc<dyn AgentHooks>) -> Self {
self.hooks.push(hook);
self
}
#[must_use]
pub fn policy(mut self, policy: Arc<dyn ToolPolicy>) -> Self {
self.policy = Some(policy);
self
}
#[must_use]
pub fn session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
self.sessions = Some(store);
self
}
#[must_use]
pub fn memory_store(mut self, store: Arc<dyn MemoryStore>) -> Self {
self.memory = Some(store);
self
}
#[must_use]
pub fn skill(mut self, skill: Skill) -> Self {
self.skills.push(skill);
self
}
#[must_use]
pub fn skills(mut self, skills: Vec<Skill>) -> Self {
self.skills.extend(skills);
self
}
#[must_use]
pub const fn compaction(mut self, compaction: Compaction) -> Self {
self.compaction = Some(compaction);
self
}
#[must_use]
pub fn delivery(mut self, delivery: Arc<dyn Delivery>) -> Self {
self.delivery = Some(delivery);
self
}
#[must_use]
pub fn heartbeat(mut self, heartbeat: Heartbeat) -> Self {
self.heartbeat = Some(heartbeat);
self
}
#[must_use]
pub const fn followups(mut self, max_delay: Duration) -> Self {
self.followups = Some(max_delay);
self
}
fn runtime(
&self,
config: AgentConfig,
client: Arc<dyn crate::client::LlmClient>,
) -> AgentRuntime {
let mut runtime = AgentRuntime::new(config, client, self.tools.clone())
.with_hooks(self.hooks.clone())
.with_skills(self.skills.clone());
if let Some(policy) = &self.policy {
runtime = runtime.with_policy(Arc::clone(policy));
}
if let Some(sessions) = &self.sessions {
runtime = runtime.with_sessions(Arc::clone(sessions));
}
if let Some(memory) = &self.memory {
runtime = runtime.with_memory(Arc::clone(memory));
}
if let Some(compaction) = self.compaction {
runtime = runtime.with_compaction(compaction);
}
if let Some(delivery) = &self.delivery {
runtime = runtime.with_delivery(Arc::clone(delivery));
}
if let Some(max_delay) = self.followups {
runtime = runtime.with_followups(max_delay);
}
runtime
}
}
impl Default for AgentPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for AgentPlugin {
fn build(self, app: AppBuilder) -> AppBuilder {
let tasks: Vec<_> = self.heartbeat.iter().map(Heartbeat::task_info).collect();
let plugin = Arc::new(self);
let active = Arc::new(OnceLock::<Arc<AgentRuntime>>::new());
let indicator = AgentHealthIndicator::new(Arc::clone(&active));
app.config_section("agent")
.jobs(autumn_web::jobs![
crate::jobs::agent_jobs::run_agent,
crate::jobs::agent_jobs::resume_agent
])
.tasks(tasks)
.health_indicator("agent", Arc::new(indicator))
.on_startup(move |state| {
let plugin = Arc::clone(&plugin);
let active = Arc::clone(&active);
async move {
let mut loaded = AgentConfig::load().map_err(AgentError::into_autumn_error)?;
apply_overrides(&mut loaded, &plugin.config);
loaded.validate().map_err(AgentError::into_autumn_error)?;
if let Some(heartbeat) = &plugin.heartbeat {
heartbeat
.validate()
.map_err(AgentError::into_autumn_error)?;
let heartbeat = heartbeat.clone();
state.extension_or_insert_with(|| HeartbeatSettings(heartbeat));
}
let client =
client_from_config(&loaded).map_err(AgentError::into_autumn_error)?;
let provider_name = loaded.provider.as_str().to_owned();
let runtime = Arc::new(plugin.runtime(loaded, client));
if active.set(Arc::clone(&runtime)).is_err() {
tracing::warn!("agent startup ran twice; keeping the first runtime");
}
state.extension_or_insert_with(|| (*runtime).clone());
tracing::info!(provider = %provider_name, "agent plugin started");
Ok(())
}
})
}
}
fn apply_overrides(loaded: &mut AgentConfig, configured: &AgentConfig) {
let defaults = AgentConfig::default();
if configured.provider != defaults.provider {
loaded.provider = configured.provider;
}
if let Some(model) = &configured.model {
loaded.model = Some(model.clone());
}
if let Some(base_url) = &configured.base_url {
loaded.base_url = Some(base_url.clone());
}
if configured.request_timeout_secs != defaults.request_timeout_secs {
loaded.request_timeout_secs = configured.request_timeout_secs;
}
if configured.max_steps != defaults.max_steps {
loaded.max_steps = configured.max_steps;
}
if configured.max_tokens != defaults.max_tokens {
loaded.max_tokens = configured.max_tokens;
}
if let Some(prompt) = &configured.system_prompt {
loaded.system_prompt = Some(prompt.clone());
}
if configured.prompt_caching != defaults.prompt_caching {
loaded.prompt_caching = configured.prompt_caching;
}
if let Some(secs) = configured.max_run_secs {
loaded.max_run_secs = Some(secs);
}
}
#[derive(Debug, Clone)]
pub struct AgentHandle {
runtime: Arc<AgentRuntime>,
}
impl AgentHandle {
#[must_use]
pub fn agent(&self) -> Agent {
self.runtime.agent()
}
pub async fn run(&self, prompt: &str) -> Result<AgentOutcome, AgentError> {
self.agent().run(prompt).await
}
pub async fn chat(
&self,
session: impl Into<SessionId>,
input: &str,
) -> Result<AgentTurn, AgentError> {
let session = session.into();
self.agent()
.run_in_session(self.runtime.sessions().as_ref(), &session, input)
.await
}
pub async fn chat_as(
&self,
scope: MemoryScope,
session: impl Into<SessionId>,
input: &str,
) -> Result<AgentTurn, AgentError> {
let session = session.into();
self.runtime
.agent_for(scope)
.run_in_session(self.runtime.sessions().as_ref(), &session, input)
.await
}
pub async fn resume(
&self,
state: RunState,
decisions: Vec<ApprovalDecision>,
) -> Result<AgentTurn, AgentError> {
let run_id = state.run_id.clone();
self.agent()
.run_id(run_id)
.resume_in_session(self.runtime.sessions().as_ref(), state, decisions)
.await
}
pub async fn resume_as(
&self,
scope: MemoryScope,
state: RunState,
decisions: Vec<ApprovalDecision>,
) -> Result<AgentTurn, AgentError> {
let run_id = state.run_id.clone();
self.runtime
.agent_for(scope)
.run_id(run_id)
.resume_in_session(self.runtime.sessions().as_ref(), state, decisions)
.await
}
#[must_use]
pub fn runtime(&self) -> &AgentRuntime {
&self.runtime
}
#[must_use]
pub fn tool_names(&self) -> Vec<&str> {
self.runtime.tool_names()
}
}
impl FromRequestParts<AppState> for AgentHandle {
type Rejection = AutumnError;
fn from_request_parts(
_parts: &mut Parts,
state: &AppState,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let runtime = state.extension::<AgentRuntime>();
async move {
runtime.map(|runtime| Self { runtime }).ok_or_else(|| {
AutumnError::service_unavailable_msg("agent plugin is not installed")
})
}
}
}
#[cfg(test)]
mod tests;