1use std::future::Future;
48use std::sync::{Arc, OnceLock};
49use std::time::Duration;
50
51use autumn_web::app::AppBuilder;
52use autumn_web::plugin::Plugin;
53use autumn_web::reexports::axum::extract::FromRequestParts;
54use autumn_web::{AppState, AutumnError};
55use http::request::Parts;
56
57use crate::agent::{Agent, AgentOutcome, AgentRuntime, AgentTurn, ApprovalDecision, RunState};
58use crate::client::client_from_config;
59use crate::config::AgentConfig;
60use crate::error::AgentError;
61use crate::health::AgentHealthIndicator;
62use crate::hooks::AgentHooks;
63use crate::ids::SessionId;
64use crate::memory::{MemoryScope, MemoryStore};
65use crate::policy::ToolPolicy;
66use crate::proactive::{Delivery, Heartbeat, HeartbeatSettings};
67use crate::session::{Compaction, SessionStore};
68use crate::skills::Skill;
69use crate::tools::Tool;
70
71pub struct AgentPlugin {
77 config: AgentConfig,
78 tools: Vec<Arc<dyn Tool>>,
79 hooks: Vec<Arc<dyn AgentHooks>>,
80 policy: Option<Arc<dyn ToolPolicy>>,
81 sessions: Option<Arc<dyn SessionStore>>,
82 memory: Option<Arc<dyn MemoryStore>>,
83 skills: Vec<Skill>,
84 compaction: Option<Compaction>,
85 delivery: Option<Arc<dyn Delivery>>,
86 heartbeat: Option<Heartbeat>,
87 followups: Option<Duration>,
88}
89
90impl std::fmt::Debug for AgentPlugin {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("AgentPlugin")
93 .field("config", &self.config)
94 .field(
95 "tools",
96 &self
97 .tools
98 .iter()
99 .map(|tool| tool.name().to_owned())
100 .collect::<Vec<_>>(),
101 )
102 .field("hooks", &self.hooks.len())
103 .field("policy", &self.policy)
104 .field("memory", &self.memory.is_some())
105 .field(
106 "skills",
107 &self
108 .skills
109 .iter()
110 .map(|skill| skill.name.as_str())
111 .collect::<Vec<_>>(),
112 )
113 .field("compaction", &self.compaction)
114 .field("heartbeat", &self.heartbeat)
115 .field("followups", &self.followups)
116 .finish_non_exhaustive()
117 }
118}
119
120impl AgentPlugin {
121 #[must_use]
126 pub fn new() -> Self {
127 Self {
128 config: AgentConfig::default(),
129 tools: Vec::new(),
130 hooks: Vec::new(),
131 policy: None,
132 sessions: None,
133 memory: None,
134 skills: Vec::new(),
135 compaction: None,
136 delivery: None,
137 heartbeat: None,
138 followups: None,
139 }
140 }
141
142 #[must_use]
145 pub fn configure(mut self, configure: impl FnOnce(&mut AgentConfig)) -> Self {
146 configure(&mut self.config);
147 self
148 }
149
150 #[must_use]
152 pub fn tool(mut self, tool: Arc<dyn Tool>) -> Self {
153 self.tools.push(tool);
154 self
155 }
156
157 #[must_use]
159 pub fn tools(mut self, tools: Vec<Arc<dyn Tool>>) -> Self {
160 self.tools.extend(tools);
161 self
162 }
163
164 #[must_use]
166 pub fn hook(mut self, hook: Arc<dyn AgentHooks>) -> Self {
167 self.hooks.push(hook);
168 self
169 }
170
171 #[must_use]
174 pub fn policy(mut self, policy: Arc<dyn ToolPolicy>) -> Self {
175 self.policy = Some(policy);
176 self
177 }
178
179 #[must_use]
182 pub fn session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
183 self.sessions = Some(store);
184 self
185 }
186
187 #[must_use]
189 pub fn memory_store(mut self, store: Arc<dyn MemoryStore>) -> Self {
190 self.memory = Some(store);
191 self
192 }
193
194 #[must_use]
196 pub fn skill(mut self, skill: Skill) -> Self {
197 self.skills.push(skill);
198 self
199 }
200
201 #[must_use]
203 pub fn skills(mut self, skills: Vec<Skill>) -> Self {
204 self.skills.extend(skills);
205 self
206 }
207
208 #[must_use]
210 pub const fn compaction(mut self, compaction: Compaction) -> Self {
211 self.compaction = Some(compaction);
212 self
213 }
214
215 #[must_use]
218 pub fn delivery(mut self, delivery: Arc<dyn Delivery>) -> Self {
219 self.delivery = Some(delivery);
220 self
221 }
222
223 #[must_use]
225 pub fn heartbeat(mut self, heartbeat: Heartbeat) -> Self {
226 self.heartbeat = Some(heartbeat);
227 self
228 }
229
230 #[must_use]
233 pub const fn followups(mut self, max_delay: Duration) -> Self {
234 self.followups = Some(max_delay);
235 self
236 }
237
238 fn runtime(
240 &self,
241 config: AgentConfig,
242 client: Arc<dyn crate::client::LlmClient>,
243 ) -> AgentRuntime {
244 let mut runtime = AgentRuntime::new(config, client, self.tools.clone())
245 .with_hooks(self.hooks.clone())
246 .with_skills(self.skills.clone());
247 if let Some(policy) = &self.policy {
248 runtime = runtime.with_policy(Arc::clone(policy));
249 }
250 if let Some(sessions) = &self.sessions {
251 runtime = runtime.with_sessions(Arc::clone(sessions));
252 }
253 if let Some(memory) = &self.memory {
254 runtime = runtime.with_memory(Arc::clone(memory));
255 }
256 if let Some(compaction) = self.compaction {
257 runtime = runtime.with_compaction(compaction);
258 }
259 if let Some(delivery) = &self.delivery {
260 runtime = runtime.with_delivery(Arc::clone(delivery));
261 }
262 if let Some(max_delay) = self.followups {
263 runtime = runtime.with_followups(max_delay);
264 }
265 runtime
266 }
267}
268
269impl Default for AgentPlugin {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275impl Plugin for AgentPlugin {
276 fn build(self, app: AppBuilder) -> AppBuilder {
283 let tasks: Vec<_> = self.heartbeat.iter().map(Heartbeat::task_info).collect();
284 let plugin = Arc::new(self);
285 let active = Arc::new(OnceLock::<Arc<AgentRuntime>>::new());
289 let indicator = AgentHealthIndicator::new(Arc::clone(&active));
290 app.config_section("agent")
291 .jobs(autumn_web::jobs![
292 crate::jobs::agent_jobs::run_agent,
293 crate::jobs::agent_jobs::resume_agent
294 ])
295 .tasks(tasks)
296 .health_indicator("agent", Arc::new(indicator))
297 .on_startup(move |state| {
298 let plugin = Arc::clone(&plugin);
299 let active = Arc::clone(&active);
300 async move {
301 let mut loaded = AgentConfig::load().map_err(AgentError::into_autumn_error)?;
304 apply_overrides(&mut loaded, &plugin.config);
305 loaded.validate().map_err(AgentError::into_autumn_error)?;
306 if let Some(heartbeat) = &plugin.heartbeat {
307 heartbeat
308 .validate()
309 .map_err(AgentError::into_autumn_error)?;
310 let heartbeat = heartbeat.clone();
311 state.extension_or_insert_with(|| HeartbeatSettings(heartbeat));
312 }
313 let client =
314 client_from_config(&loaded).map_err(AgentError::into_autumn_error)?;
315 let provider_name = loaded.provider.as_str().to_owned();
316 let runtime = Arc::new(plugin.runtime(loaded, client));
317 if active.set(Arc::clone(&runtime)).is_err() {
318 tracing::warn!("agent startup ran twice; keeping the first runtime");
319 }
320 state.extension_or_insert_with(|| (*runtime).clone());
321 tracing::info!(provider = %provider_name, "agent plugin started");
322 Ok(())
323 }
324 })
325 }
326}
327
328fn apply_overrides(loaded: &mut AgentConfig, configured: &AgentConfig) {
333 let defaults = AgentConfig::default();
334 if configured.provider != defaults.provider {
335 loaded.provider = configured.provider;
336 }
337 if let Some(model) = &configured.model {
338 loaded.model = Some(model.clone());
339 }
340 if let Some(base_url) = &configured.base_url {
341 loaded.base_url = Some(base_url.clone());
342 }
343 if configured.request_timeout_secs != defaults.request_timeout_secs {
344 loaded.request_timeout_secs = configured.request_timeout_secs;
345 }
346 if configured.max_steps != defaults.max_steps {
347 loaded.max_steps = configured.max_steps;
348 }
349 if configured.max_tokens != defaults.max_tokens {
350 loaded.max_tokens = configured.max_tokens;
351 }
352 if let Some(prompt) = &configured.system_prompt {
353 loaded.system_prompt = Some(prompt.clone());
354 }
355 if configured.prompt_caching != defaults.prompt_caching {
356 loaded.prompt_caching = configured.prompt_caching;
357 }
358 if let Some(secs) = configured.max_run_secs {
359 loaded.max_run_secs = Some(secs);
360 }
361}
362
363#[derive(Debug, Clone)]
383pub struct AgentHandle {
384 runtime: Arc<AgentRuntime>,
385}
386
387impl AgentHandle {
388 #[must_use]
392 pub fn agent(&self) -> Agent {
393 self.runtime.agent()
394 }
395
396 pub async fn run(&self, prompt: &str) -> Result<AgentOutcome, AgentError> {
403 self.agent().run(prompt).await
404 }
405
406 pub async fn chat(
412 &self,
413 session: impl Into<SessionId>,
414 input: &str,
415 ) -> Result<AgentTurn, AgentError> {
416 let session = session.into();
417 self.agent()
418 .run_in_session(self.runtime.sessions().as_ref(), &session, input)
419 .await
420 }
421
422 pub async fn chat_as(
429 &self,
430 scope: MemoryScope,
431 session: impl Into<SessionId>,
432 input: &str,
433 ) -> Result<AgentTurn, AgentError> {
434 let session = session.into();
435 self.runtime
436 .agent_for(scope)
437 .run_in_session(self.runtime.sessions().as_ref(), &session, input)
438 .await
439 }
440
441 pub async fn resume(
448 &self,
449 state: RunState,
450 decisions: Vec<ApprovalDecision>,
451 ) -> Result<AgentTurn, AgentError> {
452 let run_id = state.run_id.clone();
453 self.agent()
454 .run_id(run_id)
455 .resume_in_session(self.runtime.sessions().as_ref(), state, decisions)
456 .await
457 }
458
459 pub async fn resume_as(
466 &self,
467 scope: MemoryScope,
468 state: RunState,
469 decisions: Vec<ApprovalDecision>,
470 ) -> Result<AgentTurn, AgentError> {
471 let run_id = state.run_id.clone();
472 self.runtime
473 .agent_for(scope)
474 .run_id(run_id)
475 .resume_in_session(self.runtime.sessions().as_ref(), state, decisions)
476 .await
477 }
478
479 #[must_use]
481 pub fn runtime(&self) -> &AgentRuntime {
482 &self.runtime
483 }
484
485 #[must_use]
487 pub fn tool_names(&self) -> Vec<&str> {
488 self.runtime.tool_names()
489 }
490}
491
492impl FromRequestParts<AppState> for AgentHandle {
493 type Rejection = AutumnError;
494
495 fn from_request_parts(
499 _parts: &mut Parts,
500 state: &AppState,
501 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
502 let runtime = state.extension::<AgentRuntime>();
503 async move {
504 runtime.map(|runtime| Self { runtime }).ok_or_else(|| {
505 AutumnError::service_unavailable_msg("agent plugin is not installed")
506 })
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests;