1use std::sync::Arc;
19use std::time::Duration;
20
21use autumn_web::job::{JobContext, TrackedJobHandle};
22use autumn_web::{AppState, AutumnError, AutumnResult};
23use futures::future::BoxFuture;
24use serde::{Deserialize, Serialize};
25
26use crate::agent::{AgentOutcome, AgentRuntime, ApprovalDecision, RunState};
27use crate::error::AgentError;
28use crate::hooks::{AgentHooks, RunInfo};
29use crate::ids::{RunId, SessionId};
30use crate::memory::MemoryScope;
31use crate::policy::ToolPolicy;
32use crate::proactive::{ReportSource, deliver_outcome};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum RunOrigin {
38 #[default]
40 Request,
41 Followup,
43 Heartbeat,
45}
46
47impl From<RunOrigin> for ReportSource {
48 fn from(origin: RunOrigin) -> Self {
49 match origin {
50 RunOrigin::Request => Self::Job,
51 RunOrigin::Followup => Self::Followup,
52 RunOrigin::Heartbeat => Self::Heartbeat,
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[non_exhaustive]
63pub struct AgentRunArgs {
64 pub prompt: String,
66 #[serde(default)]
68 pub system_prompt: Option<String>,
69 #[serde(default)]
71 pub max_steps: Option<u32>,
72 #[serde(default)]
74 pub session: Option<SessionId>,
75 #[serde(default)]
77 pub memory_scope: Option<MemoryScope>,
78 #[serde(default)]
80 pub deliver: bool,
81 #[serde(default)]
83 pub origin: RunOrigin,
84 #[serde(default = "RunId::generate")]
87 pub run_id: RunId,
88 #[serde(default)]
92 pub followup_depth: u32,
93}
94
95impl AgentRunArgs {
96 #[must_use]
98 pub fn new(prompt: impl Into<String>) -> Self {
99 Self {
100 prompt: prompt.into(),
101 system_prompt: None,
102 max_steps: None,
103 session: None,
104 memory_scope: None,
105 deliver: false,
106 origin: RunOrigin::Request,
107 run_id: RunId::generate(),
108 followup_depth: 0,
109 }
110 }
111
112 #[must_use]
114 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
115 self.system_prompt = Some(prompt.into());
116 self
117 }
118
119 #[must_use]
121 pub const fn max_steps(mut self, max_steps: u32) -> Self {
122 self.max_steps = Some(max_steps);
123 self
124 }
125
126 #[must_use]
128 pub fn session(mut self, session: impl Into<Option<SessionId>>) -> Self {
129 self.session = session.into();
130 self
131 }
132
133 #[must_use]
135 pub fn memory_scope(mut self, scope: impl Into<Option<MemoryScope>>) -> Self {
136 self.memory_scope = scope.into();
137 self
138 }
139
140 #[must_use]
142 pub const fn deliver(mut self, deliver: bool) -> Self {
143 self.deliver = deliver;
144 self
145 }
146
147 #[must_use]
149 pub const fn origin(mut self, origin: RunOrigin) -> Self {
150 self.origin = origin;
151 self
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
157#[non_exhaustive]
158pub struct AgentResumeArgs {
159 pub state: RunState,
161 pub decisions: Vec<ApprovalDecision>,
163 #[serde(default)]
165 pub memory_scope: Option<MemoryScope>,
166 #[serde(default)]
168 pub deliver: bool,
169}
170
171impl AgentResumeArgs {
172 #[must_use]
174 pub const fn new(state: RunState, decisions: Vec<ApprovalDecision>) -> Self {
175 Self {
176 state,
177 decisions,
178 memory_scope: None,
179 deliver: false,
180 }
181 }
182
183 #[must_use]
185 pub fn memory_scope(mut self, scope: impl Into<Option<MemoryScope>>) -> Self {
186 self.memory_scope = scope.into();
187 self
188 }
189
190 #[must_use]
192 pub const fn deliver(mut self, deliver: bool) -> Self {
193 self.deliver = deliver;
194 self
195 }
196}
197
198#[derive(Debug)]
201struct JobProgress;
202
203impl AgentHooks for JobProgress {
204 fn before_model<'a>(
205 &'a self,
206 _request: &'a mut crate::client::ChatRequest,
207 info: &'a RunInfo,
208 ) -> BoxFuture<'a, ()> {
209 Box::pin(async move {
210 let context = JobContext::current();
211 if !context.is_tracked() {
212 return;
213 }
214 let max = info.max_steps.max(1);
215 let pct = (info.steps_used.min(max).saturating_mul(99) / max).min(99);
216 let pct = u8::try_from(pct).unwrap_or(99);
217 let message = format!("step {} of {}", info.steps_used, info.max_steps);
218 if let Err(err) = context.set_progress(pct, Some(&message)).await {
219 tracing::debug!(error = %err, "cannot record agent job progress");
220 }
221 })
222 }
223}
224
225fn record_outcome(run_id: &RunId, outcome: &AgentOutcome) {
227 match serde_json::to_value(outcome) {
228 Ok(value) => JobContext::current().set_result(value),
229 Err(err) => tracing::warn!(error = %err, "cannot serialize the agent outcome"),
230 }
231 let usage = outcome.usage();
232 tracing::info!(
233 run_id = %run_id,
234 status = outcome_status(outcome),
235 steps_used = outcome.steps_used(),
236 input_tokens = usage.input_tokens,
237 output_tokens = usage.output_tokens,
238 cache_read_tokens = usage.cache_read_tokens,
239 "background agent run finished"
240 );
241}
242
243const fn outcome_status(outcome: &AgentOutcome) -> &'static str {
244 match outcome {
245 AgentOutcome::Completed { .. } => "completed",
246 AgentOutcome::BudgetExhausted { .. } => "budget_exhausted",
247 AgentOutcome::LoopDetected { .. } => "loop_detected",
248 AgentOutcome::AwaitingApproval { .. } => "awaiting_approval",
249 }
250}
251
252pub(crate) async fn execute_run(
257 runtime: &AgentRuntime,
258 args: AgentRunArgs,
259 policy: Option<Arc<dyn ToolPolicy>>,
260) -> Result<AgentOutcome, AgentError> {
261 let scope = args.memory_scope.clone().unwrap_or_else(MemoryScope::agent);
262 let mut agent = runtime
263 .agent_with(scope, args.followup_depth)
264 .run_id(args.run_id.clone())
265 .hook(Arc::new(JobProgress));
266 if let Some(system) = &args.system_prompt {
267 agent = agent.system_prompt(system.clone());
268 }
269 if let Some(max_steps) = args.max_steps {
270 agent = agent.max_steps(max_steps);
271 }
272 if let Some(policy) = policy {
273 agent = agent.policy(policy);
274 }
275 let turn = match &args.session {
276 Some(session) => {
277 agent
278 .run_in_session(runtime.sessions().as_ref(), session, &args.prompt)
279 .await?
280 }
281 None => agent.run_turn(Vec::new(), &args.prompt).await?,
282 };
283 record_outcome(&args.run_id, &turn.outcome);
284 if args.deliver {
285 deliver_outcome(
286 runtime.delivery().as_ref(),
287 args.origin.into(),
288 &args.run_id,
289 args.session.as_ref(),
290 &turn.outcome,
291 )
292 .await;
293 }
294 Ok(turn.outcome)
295}
296
297pub(crate) async fn execute_resume(
299 runtime: &AgentRuntime,
300 args: AgentResumeArgs,
301) -> Result<AgentOutcome, AgentError> {
302 let scope = args.memory_scope.clone().unwrap_or_else(MemoryScope::agent);
303 let run_id = args.state.run_id.clone();
304 let session = args.state.session_id.clone();
305 let agent = runtime
306 .agent_for(scope)
307 .run_id(run_id.clone())
308 .hook(Arc::new(JobProgress));
309 let turn = agent
310 .resume_in_session(runtime.sessions().as_ref(), args.state, args.decisions)
311 .await?;
312 record_outcome(&run_id, &turn.outcome);
313 if args.deliver {
314 deliver_outcome(
315 runtime.delivery().as_ref(),
316 ReportSource::Resume,
317 &run_id,
318 session.as_ref(),
319 &turn.outcome,
320 )
321 .await;
322 }
323 Ok(turn.outcome)
324}
325
326fn runtime_from(state: &AppState) -> AutumnResult<Arc<AgentRuntime>> {
327 state
328 .extension::<AgentRuntime>()
329 .ok_or_else(|| AutumnError::service_unavailable_msg("agent plugin is not installed"))
330}
331
332#[doc(hidden)]
339#[allow(missing_docs)]
340pub(crate) mod agent_jobs {
341 use super::{AgentResumeArgs, AgentRunArgs, execute_resume, execute_run, runtime_from};
342 use autumn_web::{AppState, AutumnResult};
343
344 #[autumn_web::job(name = "agent_run", max_attempts = 3, backoff_ms = 1_000)]
348 pub async fn run_agent(state: AppState, args: AgentRunArgs) -> AutumnResult<()> {
349 let runtime = runtime_from(&state)?;
350 execute_run(&runtime, args, None)
351 .await
352 .map(|_| ())
353 .map_err(|err| {
354 tracing::error!(error = %err, "background agent run failed");
355 err.into_autumn_error()
356 })
357 }
358
359 #[autumn_web::job(name = "agent_resume", max_attempts = 1)]
362 pub async fn resume_agent(state: AppState, args: AgentResumeArgs) -> AutumnResult<()> {
363 let runtime = runtime_from(&state)?;
364 execute_resume(&runtime, args)
365 .await
366 .map(|_| ())
367 .map_err(|err| {
368 tracing::error!(error = %err, "background agent resume failed");
369 err.into_autumn_error()
370 })
371 }
372}
373
374pub async fn enqueue_agent_run(args: AgentRunArgs) -> AutumnResult<()> {
381 agent_jobs::RunAgentJob::enqueue(args).await
382}
383
384pub async fn enqueue_agent_run_in(args: AgentRunArgs, delay: Duration) -> AutumnResult<()> {
390 agent_jobs::RunAgentJob::enqueue_in(args, delay).await
391}
392
393pub async fn enqueue_agent_run_tracked(args: AgentRunArgs) -> AutumnResult<TrackedJobHandle> {
404 agent_jobs::RunAgentJob::enqueue_tracked(args).await
405}
406
407pub async fn enqueue_agent_resume(args: AgentResumeArgs) -> AutumnResult<()> {
413 agent_jobs::ResumeAgentJob::enqueue(args).await
414}
415
416pub async fn enqueue_agent_resume_tracked(args: AgentResumeArgs) -> AutumnResult<TrackedJobHandle> {
422 agent_jobs::ResumeAgentJob::enqueue_tracked(args).await
423}
424
425#[cfg(test)]
426mod tests;