1use std::{future::Future, path::PathBuf, sync::Arc};
4
5use async_trait::async_trait;
6use thiserror::Error;
7use tokio_util::sync::CancellationToken;
8
9use crate::{
10 ApprovalGate, ApprovalRequest, ContextPolicy, CoreEvent, EventSink, HistoryLimits, Message,
11 PROGRESS_INTERVAL, ProgressSink, Provider, ProviderError, ProviderErrorKind, ProviderRequest,
12 TextDeltaSink, ToolApprovals, ToolCall, ToolContext, ToolError, ToolFailure, ToolOutput,
13 ToolRegistry, TurnInput, Usage,
14};
15
16const DENIED: &str = "tool call denied by policy or user";
18
19#[derive(Debug, Clone)]
21pub struct AgentConfig {
22 pub system_prompt: String,
23 pub max_steps: usize,
25 pub history_limits: HistoryLimits,
26}
27
28#[derive(Debug, Clone)]
30pub struct TurnOutcome {
31 pub steps: usize,
32 pub usage: Usage,
33}
34
35#[derive(Debug, Error)]
37pub enum AgentError {
38 #[error("turn cancelled")]
39 Cancelled,
40 #[error("{0}")]
41 Provider(String),
42 #[error("{0}")]
43 ContextLimit(String),
44 #[error("agent reached its maximum step count")]
45 StepLimit,
46 #[error("{0}")]
47 HistoryLimit(String),
48 #[error("{0}")]
49 ResponseLimit(String),
50 #[error("{0}")]
51 ToolLimit(String),
52 #[error("{0}")]
53 Internal(String),
54}
55
56pub struct AgentRuntime {
71 provider: Arc<dyn Provider>,
72 tools: Arc<ToolRegistry>,
73 context: Arc<dyn ContextPolicy>,
74 config: AgentConfig,
75 workspace: PathBuf,
76}
77
78impl AgentRuntime {
79 pub fn new(
80 provider: Arc<dyn Provider>,
81 tools: Arc<ToolRegistry>,
82 context: Arc<dyn ContextPolicy>,
83 config: AgentConfig,
84 workspace: PathBuf,
85 ) -> Self {
86 Self {
87 provider,
88 tools,
89 context,
90 config,
91 workspace,
92 }
93 }
94
95 pub fn model(&self) -> &str {
96 self.provider.model()
97 }
98
99 pub async fn run_turn(
100 &self,
101 history: &mut Vec<Message>,
102 prompt: impl Into<TurnInput>,
103 sink: Arc<dyn EventSink>,
104 approvals: Arc<dyn ApprovalGate>,
105 cancellation: CancellationToken,
106 ) -> Result<TurnOutcome, AgentError> {
107 let checkpoint = history.clone();
108 let result = self
109 .run_turn_inner(history, prompt.into(), sink, approvals, cancellation)
110 .await;
111 if result.is_err() {
112 *history = checkpoint;
113 }
114 result
115 }
116
117 async fn run_turn_inner(
118 &self,
119 history: &mut Vec<Message>,
120 prompt: TurnInput,
121 sink: Arc<dyn EventSink>,
122 approvals: Arc<dyn ApprovalGate>,
123 cancellation: CancellationToken,
124 ) -> Result<TurnOutcome, AgentError> {
125 if cancellation.is_cancelled() {
126 return Err(AgentError::Cancelled);
127 }
128 history.push(Message::User {
129 content: prompt.text,
130 images: prompt.images,
131 });
132 self.enforce_history_limits(history, sink.as_ref()).await?;
133 let specs = self.tools.specs();
134 let mut usage = Usage::default();
135
136 for step in 1..=self.config.max_steps {
137 if cancellation.is_cancelled() {
138 return Err(AgentError::Cancelled);
139 }
140 let selection = self
141 .context
142 .select(history, &self.config.system_prompt, &specs)
143 .map_err(|error| AgentError::ContextLimit(error.to_string()))?;
144 if selection.removed_messages > 0 {
145 sink.emit(CoreEvent::ContextCompacted {
146 before_tokens: selection.before_tokens,
147 after_tokens: selection.after_tokens,
148 removed_messages: selection.removed_messages,
149 })
150 .await?;
151 }
152 let delta_sink: Arc<dyn TextDeltaSink> = Arc::new(ForwardDeltas {
153 sink: Arc::clone(&sink),
154 });
155 let response = self
156 .provider
157 .complete(
158 ProviderRequest {
159 system_prompt: self.config.system_prompt.clone(),
160 messages: selection.messages,
161 tools: specs.clone(),
162 },
163 delta_sink,
164 cancellation.child_token(),
165 )
166 .await
167 .map_err(map_provider_error)?;
168 usage.add(&response.usage);
169 sink.emit(CoreEvent::AssistantCompleted {
170 content: response.content.clone(),
171 })
172 .await?;
173 let calls = response.tool_calls.clone();
174 history.push(Message::Assistant {
175 content: response.content,
176 tool_calls: response.tool_calls,
177 });
178 self.enforce_history_limits(history, sink.as_ref()).await?;
179 if calls.is_empty() {
180 return Ok(TurnOutcome { steps: step, usage });
181 }
182
183 for call in calls {
184 if cancellation.is_cancelled() {
185 return Err(AgentError::Cancelled);
186 }
187 sink.emit(CoreEvent::ToolProposed {
188 call_id: call.id.clone(),
189 name: call.name.clone(),
190 arguments: call.arguments.clone(),
191 })
192 .await?;
193 let Some(tool) = self.tools.get(&call.name) else {
194 let error = ToolError::new(
195 ToolFailure::UnknownTool,
196 format!("unknown tool: {}", call.name),
197 );
198 self.record_tool_error(history, sink.as_ref(), &call, error)
199 .await?;
200 self.enforce_history_limits(history, sink.as_ref()).await?;
201 continue;
202 };
203 let risk = match tool.risk(&call.arguments) {
204 Ok(risk) => risk,
205 Err(error) => {
206 self.record_tool_error(history, sink.as_ref(), &call, error)
207 .await?;
208 self.enforce_history_limits(history, sink.as_ref()).await?;
209 continue;
210 }
211 };
212 let summary = match tool.approval_summary(&call.arguments) {
213 Ok(summary) => summary,
214 Err(error) => {
215 self.record_tool_error(history, sink.as_ref(), &call, error)
216 .await?;
217 self.enforce_history_limits(history, sink.as_ref()).await?;
218 continue;
219 }
220 };
221 let approved = approvals
222 .approve(
223 ApprovalRequest {
224 call_id: call.id.clone(),
225 name: call.name.clone(),
226 risk,
227 cwd: self.workspace.clone(),
228 summary,
229 },
230 cancellation.child_token(),
231 )
232 .await?;
233 let output = if approved {
234 sink.emit(CoreEvent::ToolStarted {
235 call_id: call.id.clone(),
236 name: call.name.clone(),
237 })
238 .await?;
239 let progress = ProgressSink::buffered();
240 let execution = tool.execute(
241 call.arguments.clone(),
242 ToolContext {
243 call_id: call.id.clone(),
244 workspace: self.workspace.clone(),
245 cancellation: cancellation.child_token(),
246 background_job: false,
247 progress: progress.clone(),
248 approvals: ToolApprovals::new(Arc::clone(&approvals), call.id.clone()),
249 },
250 );
251 forward_progress(execution, &progress, sink.as_ref(), &call.id)
252 .await
253 .unwrap_or_else(ToolOutput::from)
254 } else {
255 ToolOutput::failed(ToolFailure::Denied, DENIED)
256 };
257 sink.emit(CoreEvent::ToolCompleted {
258 call_id: call.id.clone(),
259 name: call.name.clone(),
260 output: output.clone(),
261 })
262 .await?;
263 let is_error = output.is_error();
264 history.push(Message::Tool {
265 call_id: call.id,
266 name: call.name,
267 content: output.content,
268 is_error,
269 });
270 self.enforce_history_limits(history, sink.as_ref()).await?;
271 }
272 }
273 Err(AgentError::StepLimit)
274 }
275
276 async fn record_tool_error(
278 &self,
279 history: &mut Vec<Message>,
280 sink: &dyn EventSink,
281 call: &ToolCall,
282 error: ToolError,
283 ) -> Result<(), AgentError> {
284 let output = ToolOutput::from(error);
285 sink.emit(CoreEvent::ToolCompleted {
286 call_id: call.id.clone(),
287 name: call.name.clone(),
288 output: output.clone(),
289 })
290 .await?;
291 history.push(Message::Tool {
292 call_id: call.id.clone(),
293 name: call.name.clone(),
294 content: output.content,
295 is_error: true,
296 });
297 Ok(())
298 }
299
300 pub(crate) async fn enforce_history_limits(
301 &self,
302 history: &mut Vec<Message>,
303 sink: &dyn EventSink,
304 ) -> Result<(), AgentError> {
305 crate::history::enforce_limits(history, &self.config.history_limits, sink).await
306 }
307}
308
309async fn forward_progress<T>(
313 execution: impl Future<Output = T>,
314 progress: &ProgressSink,
315 sink: &dyn EventSink,
316 call_id: &str,
317) -> T {
318 let mut execution = std::pin::pin!(execution);
319 let mut ticker = tokio::time::interval_at(
320 tokio::time::Instant::now() + PROGRESS_INTERVAL,
321 PROGRESS_INTERVAL,
322 );
323 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
324 let mut last_sent: Option<tokio::time::Instant> = None;
325 let mut forwarding = true;
326 let result = loop {
327 tokio::select! {
328 biased;
329 result = &mut execution => break result,
330 _ = ticker.tick(), if forwarding => {
331 if let Some(text) = progress.take() {
332 let event = CoreEvent::ToolProgress { call_id: call_id.to_owned(), text };
333 forwarding = sink.emit(event).await.is_ok();
334 last_sent = Some(tokio::time::Instant::now());
335 }
336 }
337 }
338 };
339 if forwarding
341 && last_sent.is_none_or(|sent| sent.elapsed() >= PROGRESS_INTERVAL)
342 && let Some(text) = progress.take()
343 {
344 let _ = sink
345 .emit(CoreEvent::ToolProgress {
346 call_id: call_id.to_owned(),
347 text,
348 })
349 .await;
350 }
351 result
352}
353
354fn map_provider_error(error: ProviderError) -> AgentError {
355 match error.kind {
356 ProviderErrorKind::Provider => AgentError::Provider(error.message),
357 ProviderErrorKind::ResponseLimit => AgentError::ResponseLimit(error.message),
358 ProviderErrorKind::ToolLimit => AgentError::ToolLimit(error.message),
359 ProviderErrorKind::Cancelled => AgentError::Cancelled,
360 }
361}
362
363struct ForwardDeltas {
364 sink: Arc<dyn EventSink>,
365}
366
367#[async_trait]
368impl TextDeltaSink for ForwardDeltas {
369 async fn push(&self, delta: &str) -> Result<(), ProviderError> {
370 self.sink
371 .emit(CoreEvent::AssistantDelta {
372 content: delta.to_owned(),
373 })
374 .await
375 .map_err(|error| match error {
376 AgentError::Cancelled => {
377 ProviderError::new(ProviderErrorKind::Cancelled, "turn cancelled")
378 }
379 AgentError::ResponseLimit(message) => {
380 ProviderError::new(ProviderErrorKind::ResponseLimit, message)
381 }
382 AgentError::ToolLimit(message) => {
383 ProviderError::new(ProviderErrorKind::ToolLimit, message)
384 }
385 error => ProviderError::new(ProviderErrorKind::Provider, error.to_string()),
386 })
387 }
388}
389
390#[cfg(test)]
391mod tests;