Skip to main content

serdes_ai_agent/
run.rs

1//! Agent run execution.
2//!
3//! This module contains the core execution logic for agent runs.
4
5use crate::agent::{Agent, EndStrategy};
6use crate::context::{generate_run_id, RunContext, RunUsage, UsageLimits};
7use crate::errors::{AgentRunError, OutputParseError, OutputValidationError};
8use chrono::Utc;
9use serde_json::Value as JsonValue;
10use serdes_ai_core::messages::{RetryPromptPart, ToolCallArgs, ToolReturnPart, UserContent};
11use serdes_ai_core::{
12    FinishReason, ModelRequest, ModelRequestPart, ModelResponse, ModelResponsePart, ModelSettings,
13};
14use serdes_ai_models::ModelRequestParameters;
15use serdes_ai_tools::{ToolError, ToolReturn};
16use std::sync::Arc;
17use tokio_util::sync::CancellationToken;
18
19/// Context compression strategy.
20#[derive(Debug, Clone, Default)]
21pub enum CompressionStrategy {
22    /// Keep only the last ~30k tokens worth of messages.
23    #[default]
24    Truncate,
25    /// Use LLM to summarize older messages into condensed form.
26    Summarize,
27}
28
29/// Context compression configuration.
30#[derive(Debug, Clone)]
31pub struct ContextCompression {
32    /// Compression strategy to use.
33    pub strategy: CompressionStrategy,
34    /// Trigger threshold (0.0-1.0). Default: 0.75
35    pub threshold: f64,
36    /// Target token count for truncation/summarization. Default: 30_000
37    pub target_tokens: usize,
38}
39
40impl Default for ContextCompression {
41    fn default() -> Self {
42        Self {
43            strategy: CompressionStrategy::Truncate,
44            threshold: 0.75,
45            target_tokens: 30_000,
46        }
47    }
48}
49
50/// Options for a run.
51#[derive(Debug, Clone, Default)]
52pub struct RunOptions {
53    /// Override model settings.
54    pub model_settings: Option<ModelSettings>,
55    /// Message history to continue from.
56    pub message_history: Option<Vec<ModelRequest>>,
57    /// Usage limits for this run.
58    pub usage_limits: Option<crate::context::UsageLimits>,
59    /// Custom metadata.
60    pub metadata: Option<JsonValue>,
61    /// Context compression configuration.
62    pub compression: Option<ContextCompression>,
63}
64
65impl RunOptions {
66    /// Create new default options.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Set model settings override.
72    pub fn model_settings(mut self, settings: ModelSettings) -> Self {
73        self.model_settings = Some(settings);
74        self
75    }
76
77    /// Set message history.
78    pub fn message_history(mut self, history: Vec<ModelRequest>) -> Self {
79        self.message_history = Some(history);
80        self
81    }
82
83    /// Set metadata.
84    pub fn metadata(mut self, metadata: JsonValue) -> Self {
85        self.metadata = Some(metadata);
86        self
87    }
88
89    /// Set context compression configuration.
90    pub fn with_compression(mut self, config: ContextCompression) -> Self {
91        self.compression = Some(config);
92        self
93    }
94}
95
96/// Result of an agent run.
97#[derive(Debug, Clone)]
98pub struct AgentRunResult<Output> {
99    /// The output data.
100    pub output: Output,
101    /// Message history.
102    pub messages: Vec<ModelRequest>,
103    /// All model responses.
104    pub responses: Vec<ModelResponse>,
105    /// Usage for this run.
106    pub usage: RunUsage,
107    /// Run ID.
108    pub run_id: String,
109    /// Finish reason.
110    pub finish_reason: FinishReason,
111    /// Metadata.
112    pub metadata: Option<JsonValue>,
113}
114
115impl<Output> AgentRunResult<Output> {
116    /// Get the output.
117    pub fn output(&self) -> &Output {
118        &self.output
119    }
120
121    /// Consume and return output.
122    pub fn into_output(self) -> Output {
123        self.output
124    }
125}
126
127/// Active agent run that can be iterated.
128///
129/// # Cancellation
130///
131/// Use [`AgentRun::new_with_cancel`] to create a run with cancellation support.
132/// The run will check for cancellation at the start of each step and before
133/// each tool execution.
134pub struct AgentRun<'a, Deps, Output> {
135    agent: &'a Agent<Deps, Output>,
136    #[allow(dead_code)]
137    deps: Arc<Deps>,
138    state: AgentRunState<Output>,
139    ctx: RunContext<Deps>,
140    run_usage_limits: Option<UsageLimits>,
141    /// Cancellation token for this run (if cancellation is enabled).
142    cancel_token: Option<CancellationToken>,
143}
144
145struct AgentRunState<Output> {
146    messages: Vec<ModelRequest>,
147    responses: Vec<ModelResponse>,
148    usage: RunUsage,
149    run_id: String,
150    step: u32,
151    output_retries: u32,
152    final_output: Option<Output>,
153    finished: bool,
154    finish_reason: Option<FinishReason>,
155}
156
157/// Canonicalize tool-call arguments in a model response before persisting it.
158///
159/// Models can emit malformed JSON-ish strings in tool call args. We execute tools
160/// with `to_json()` (which repairs/falls back), and we must persist that canonical
161/// form into history so subsequent provider requests don't carry raw malformed args.
162fn canonicalize_tool_call_args_in_response(response: &mut ModelResponse) {
163    for part in &mut response.parts {
164        if let ModelResponsePart::ToolCall(tc) = part {
165            let repaired = tc.args.to_json();
166            tc.args = ToolCallArgs::Json(repaired);
167        }
168    }
169}
170
171/// Result of a single step.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum StepResult {
174    /// Continue to next step.
175    Continue,
176    /// Tools were executed.
177    ToolsExecuted(usize),
178    /// Output is ready.
179    OutputReady,
180    /// Retrying output validation.
181    RetryingOutput,
182    /// Run is finished.
183    Finished,
184}
185
186impl<'a, Deps, Output> AgentRun<'a, Deps, Output>
187where
188    Deps: Send + Sync + 'static,
189    Output: Send + Sync + 'static,
190{
191    /// Create a new agent run.
192    pub async fn new(
193        agent: &'a Agent<Deps, Output>,
194        prompt: UserContent,
195        deps: Deps,
196        options: RunOptions,
197    ) -> Result<Self, AgentRunError> {
198        let run_id = generate_run_id();
199        let deps = Arc::new(deps);
200
201        let model_settings = options
202            .model_settings
203            .unwrap_or_else(|| agent.model_settings.clone());
204
205        let ctx = RunContext {
206            deps: deps.clone(),
207            run_id: run_id.clone(),
208            start_time: Utc::now(),
209            model_name: agent.model().name().to_string(),
210            model_settings: model_settings.clone(),
211            tool_name: None,
212            tool_call_id: None,
213            retry_count: 0,
214            metadata: options.metadata.clone(),
215        };
216
217        // Build initial messages
218        let mut messages = options.message_history.unwrap_or_default();
219
220        // Build system prompt
221        let system_prompt = agent.build_system_prompt(&ctx).await;
222        if !system_prompt.is_empty() {
223            let mut req = ModelRequest::new();
224            req.add_system_prompt(system_prompt);
225            messages.push(req);
226        }
227
228        // Add user prompt
229        let mut user_req = ModelRequest::new();
230        user_req.add_user_prompt(prompt);
231        messages.push(user_req);
232
233        Ok(Self {
234            agent,
235            deps,
236            state: AgentRunState {
237                messages,
238                responses: Vec::new(),
239                usage: RunUsage::new(),
240                run_id,
241                step: 0,
242                output_retries: 0,
243                final_output: None,
244                finished: false,
245                finish_reason: None,
246            },
247            ctx,
248            run_usage_limits: options.usage_limits,
249            cancel_token: None,
250        })
251    }
252
253    /// Create a new agent run with cancellation support.
254    ///
255    /// The provided `CancellationToken` can be used to cancel the agent run
256    /// mid-execution. When cancelled:
257    /// - The current step will complete (model request or tool execution)
258    /// - The next step will return `AgentRunError::Cancelled`
259    ///
260    /// # Example
261    ///
262    /// ```ignore
263    /// use tokio_util::sync::CancellationToken;
264    ///
265    /// let cancel_token = CancellationToken::new();
266    /// let mut run = AgentRun::new_with_cancel(
267    ///     &agent,
268    ///     "Hello!".into(),
269    ///     deps,
270    ///     RunOptions::default(),
271    ///     cancel_token.clone(),
272    /// ).await?;
273    ///
274    /// // Cancel from another task
275    /// cancel_token.cancel();
276    /// ```
277    pub async fn new_with_cancel(
278        agent: &'a Agent<Deps, Output>,
279        prompt: UserContent,
280        deps: Deps,
281        options: RunOptions,
282        cancel_token: CancellationToken,
283    ) -> Result<Self, AgentRunError> {
284        let run_id = generate_run_id();
285        let deps = Arc::new(deps);
286
287        let model_settings = options
288            .model_settings
289            .unwrap_or_else(|| agent.model_settings.clone());
290
291        let ctx = RunContext {
292            deps: deps.clone(),
293            run_id: run_id.clone(),
294            start_time: Utc::now(),
295            model_name: agent.model().name().to_string(),
296            model_settings: model_settings.clone(),
297            tool_name: None,
298            tool_call_id: None,
299            retry_count: 0,
300            metadata: options.metadata.clone(),
301        };
302
303        // Build initial messages
304        let mut messages = options.message_history.unwrap_or_default();
305
306        // Build system prompt
307        let system_prompt = agent.build_system_prompt(&ctx).await;
308        if !system_prompt.is_empty() {
309            let mut req = ModelRequest::new();
310            req.add_system_prompt(system_prompt);
311            messages.push(req);
312        }
313
314        // Add user prompt
315        let mut user_req = ModelRequest::new();
316        user_req.add_user_prompt(prompt);
317        messages.push(user_req);
318
319        Ok(Self {
320            agent,
321            deps,
322            state: AgentRunState {
323                messages,
324                responses: Vec::new(),
325                usage: RunUsage::new(),
326                run_id,
327                step: 0,
328                output_retries: 0,
329                final_output: None,
330                finished: false,
331                finish_reason: None,
332            },
333            ctx,
334            run_usage_limits: options.usage_limits,
335            cancel_token: Some(cancel_token),
336        })
337    }
338
339    /// Run to completion.
340    pub async fn run_to_completion(mut self) -> Result<AgentRunResult<Output>, AgentRunError> {
341        while !self.state.finished {
342            self.step().await?;
343        }
344        self.finalize()
345    }
346
347    /// Execute one step.
348    ///
349    /// If cancellation is enabled and the token has been triggered,
350    /// this will return `AgentRunError::Cancelled`.
351    pub async fn step(&mut self) -> Result<StepResult, AgentRunError> {
352        if self.state.finished {
353            return Ok(StepResult::Finished);
354        }
355
356        // Check for cancellation at the start of each step
357        if let Some(ref token) = self.cancel_token {
358            if token.is_cancelled() {
359                return Err(AgentRunError::Cancelled);
360            }
361        }
362
363        self.state.step += 1;
364
365        // Check usage limits
366        if let Some(limits) = &self.agent.usage_limits {
367            limits.check(&self.state.usage)?;
368            limits.check_time(self.ctx.elapsed_seconds() as u64)?;
369        }
370
371        if let Some(limits) = &self.run_usage_limits {
372            limits.check(&self.state.usage)?;
373            limits.check_time(self.ctx.elapsed_seconds() as u64)?;
374        }
375
376        // Get cached tool definitions (pre-computed at build time - no cloning!)
377        let tool_defs = self.agent.tool_definitions();
378
379        // Build request parameters
380        let params = ModelRequestParameters::new()
381            .with_tools_arc(tool_defs)
382            .with_allow_text(true);
383
384        // Process message history
385        let messages = self.process_history().await;
386
387        // Make model request
388        let mut response = self
389            .agent
390            .model()
391            .request(&messages, &self.ctx.model_settings, &params)
392            .await?;
393
394        // Persist canonical tool args to avoid carrying malformed raw args in history.
395        canonicalize_tool_call_args_in_response(&mut response);
396
397        // Update usage
398        if let Some(usage) = &response.usage {
399            self.state.usage.add_request(usage.clone());
400        }
401
402        // Store response
403        if response.finish_reason.is_some() {
404            self.state.finish_reason = response.finish_reason;
405        }
406        self.state.responses.push(response.clone());
407
408        // Process response
409        self.process_response(response).await
410    }
411
412    async fn process_history(&self) -> Vec<ModelRequest> {
413        let mut messages = self.state.messages.clone();
414
415        // Apply history processors
416        for processor in &self.agent.history_processors {
417            messages = processor.process(&self.ctx, messages).await;
418        }
419
420        messages
421    }
422
423    async fn process_response(
424        &mut self,
425        response: ModelResponse,
426    ) -> Result<StepResult, AgentRunError> {
427        let mut tool_calls = Vec::new();
428        let mut found_output = None;
429
430        for part in &response.parts {
431            match part {
432                ModelResponsePart::Text(text) => {
433                    if !text.content.is_empty() {
434                        // Try to parse as output
435                        match self.agent.output_schema.parse_text(&text.content) {
436                            Ok(output) => found_output = Some(output),
437                            Err(OutputParseError::NotFound) => {}
438                            Err(_) => {} // Try other parts
439                        }
440                    }
441                }
442                ModelResponsePart::ToolCall(tc) => {
443                    // Check if this is the output tool
444                    if self.agent.is_output_tool(&tc.tool_name) {
445                        let args = tc.args.to_json();
446                        if let Ok(output) = self
447                            .agent
448                            .output_schema
449                            .parse_tool_call(&tc.tool_name, &args)
450                        {
451                            found_output = Some(output);
452                            continue;
453                        }
454                    }
455
456                    // Regular tool call
457                    tool_calls.push(tc.clone());
458                }
459                ModelResponsePart::Thinking(_) => {
460                    // Thinking parts are recorded but not processed
461                }
462                ModelResponsePart::File(_) => {
463                    // File parts are recorded but not processed as output
464                }
465                ModelResponsePart::BuiltinToolCall(_) => {
466                    // Builtin tool calls are handled by the provider
467                }
468            }
469        }
470
471        // Execute tool calls FIRST - they take priority over text output.
472        // This matches the behavior in stream.rs and prevents the agent from
473        // stopping early when the model returns both explanatory text AND tool
474        // calls in the same response. This is especially important when
475        // Output=String, since any text would be valid "output".
476        if !tool_calls.is_empty() {
477            let count = tool_calls.len();
478            let returns = self.execute_tool_calls(tool_calls).await;
479            self.add_tool_returns(returns)?;
480            return Ok(StepResult::ToolsExecuted(count));
481        }
482
483        // Handle output if found (only when no tool calls are pending)
484        if let Some(output) = found_output {
485            match self.validate_output(output).await {
486                Ok(validated) => {
487                    self.state.final_output = Some(validated);
488
489                    // Early strategy: stop immediately
490                    if self.agent.end_strategy == EndStrategy::Early {
491                        self.state.finished = true;
492                        return Ok(StepResult::OutputReady);
493                    }
494                }
495                Err(e) => {
496                    self.state.output_retries += 1;
497                    if self.state.output_retries > self.agent.max_output_retries {
498                        return Err(AgentRunError::OutputValidationFailed(e));
499                    }
500
501                    // Add retry message
502                    self.add_retry_message(e)?;
503                    return Ok(StepResult::RetryingOutput);
504                }
505            }
506        }
507
508        // Check if we should finish
509        if response.finish_reason == Some(FinishReason::Stop) {
510            if self.state.final_output.is_some() {
511                self.state.finished = true;
512                return Ok(StepResult::Finished);
513            }
514
515            // No output and model stopped - try to use text content as output
516            if let Some(text) = response.parts.iter().find_map(|p| match p {
517                ModelResponsePart::Text(t) if !t.content.is_empty() => Some(&t.content),
518                _ => None,
519            }) {
520                // Try one more time to parse
521                if let Ok(output) = self.agent.output_schema.parse_text(text) {
522                    match self.validate_output(output).await {
523                        Ok(validated) => {
524                            self.state.final_output = Some(validated);
525                            self.state.finished = true;
526                            return Ok(StepResult::Finished);
527                        }
528                        Err(e) => {
529                            return Err(AgentRunError::OutputValidationFailed(e));
530                        }
531                    }
532                }
533            }
534
535            return Err(AgentRunError::UnexpectedStop);
536        }
537
538        Ok(StepResult::Continue)
539    }
540
541    async fn execute_tool_calls(
542        &mut self,
543        calls: Vec<serdes_ai_core::messages::ToolCallPart>,
544    ) -> Vec<(String, Option<String>, Result<ToolReturn, ToolError>)> {
545        if self.agent.parallel_tool_calls {
546            self.execute_tools_parallel(calls).await
547        } else {
548            self.execute_tools_sequential(calls).await
549        }
550    }
551
552    /// Execute tool calls sequentially (original behavior).
553    ///
554    /// Checks for cancellation before each tool execution.
555    async fn execute_tools_sequential(
556        &mut self,
557        calls: Vec<serdes_ai_core::messages::ToolCallPart>,
558    ) -> Vec<(String, Option<String>, Result<ToolReturn, ToolError>)> {
559        let mut returns = Vec::new();
560
561        for tc in calls {
562            // Check for cancellation before each tool
563            if let Some(ref token) = self.cancel_token {
564                if token.is_cancelled() {
565                    returns.push((
566                        tc.tool_name.clone(),
567                        tc.tool_call_id.clone(),
568                        Err(ToolError::Cancelled),
569                    ));
570                    continue;
571                }
572            }
573
574            self.state.usage.record_tool_call();
575
576            let tool = match self.agent.find_tool(&tc.tool_name) {
577                Some(t) => t,
578                None => {
579                    returns.push((
580                        tc.tool_name.clone(),
581                        tc.tool_call_id.clone(),
582                        Err(ToolError::NotFound(tc.tool_name.clone())),
583                    ));
584                    continue;
585                }
586            };
587
588            // Create tool context
589            let tool_ctx = self.ctx.for_tool(&tc.tool_name, tc.tool_call_id.clone());
590
591            // Execute with retries
592            let args = tc.args.to_json();
593            let mut retries = 0;
594            let result = loop {
595                match tool.executor.execute(args.clone(), &tool_ctx).await {
596                    Ok(r) => break Ok(r),
597                    Err(e) if e.is_retryable() && retries < tool.max_retries => {
598                        retries += 1;
599                        continue;
600                    }
601                    Err(e) => break Err(e),
602                }
603            };
604
605            returns.push((tc.tool_name.clone(), tc.tool_call_id.clone(), result));
606        }
607
608        returns
609    }
610
611    /// Execute tool calls in parallel.
612    async fn execute_tools_parallel(
613        &mut self,
614        calls: Vec<serdes_ai_core::messages::ToolCallPart>,
615    ) -> Vec<(String, Option<String>, Result<ToolReturn, ToolError>)> {
616        use futures::future::join_all;
617
618        // Record all tool calls upfront
619        for _ in &calls {
620            self.state.usage.record_tool_call();
621        }
622
623        // Build futures for each tool call
624        let futures: Vec<_> = calls
625            .into_iter()
626            .map(|tc| {
627                let tool_name = tc.tool_name.clone();
628                let tool_call_id = tc.tool_call_id.clone();
629                let args = tc.args.to_json();
630
631                // Look up tool (we need to clone Arc references for async move)
632                let tool = self.agent.find_tool(&tc.tool_name).cloned();
633                let tool_ctx = self.ctx.for_tool(&tc.tool_name, tc.tool_call_id.clone());
634
635                async move {
636                    let tool = match tool {
637                        Some(t) => t,
638                        None => {
639                            return (
640                                tool_name.clone(),
641                                tool_call_id,
642                                Err(ToolError::NotFound(tool_name)),
643                            );
644                        }
645                    };
646
647                    // Execute with retries
648                    let max_retries = tool.max_retries;
649                    let executor = tool.executor;
650                    let mut retries = 0;
651
652                    let result = loop {
653                        match executor.execute(args.clone(), &tool_ctx).await {
654                            Ok(r) => break Ok(r),
655                            Err(e) if e.is_retryable() && retries < max_retries => {
656                                retries += 1;
657                                continue;
658                            }
659                            Err(e) => break Err(e),
660                        }
661                    };
662
663                    (tool_name, tool_call_id, result)
664                }
665            })
666            .collect();
667
668        // Execute all futures, respecting concurrency limit if set
669        if let Some(max_concurrent) = self.agent.max_concurrent_tools {
670            self.execute_with_semaphore(futures, max_concurrent).await
671        } else {
672            join_all(futures).await
673        }
674    }
675
676    /// Execute futures with a concurrency limit using a semaphore.
677    ///
678    /// Uses `join_all` to preserve the order of results while limiting
679    /// how many futures execute concurrently via a semaphore.
680    async fn execute_with_semaphore<F, T>(&self, futures: Vec<F>, max_concurrent: usize) -> Vec<T>
681    where
682        F: std::future::Future<Output = T> + Send,
683        T: Send,
684    {
685        use futures::future::join_all;
686        use std::sync::Arc;
687        use tokio::sync::Semaphore;
688
689        let semaphore = Arc::new(Semaphore::new(max_concurrent));
690
691        let wrapped_futures: Vec<_> = futures
692            .into_iter()
693            .map(|fut| {
694                let sem = Arc::clone(&semaphore);
695                async move {
696                    // Acquire permit before executing - this limits concurrency
697                    let _permit = sem.acquire().await.expect("Semaphore closed unexpectedly");
698                    fut.await
699                    // Permit is dropped here, allowing another future to proceed
700                }
701            })
702            .collect();
703
704        // join_all preserves order - results[i] corresponds to futures[i]
705        join_all(wrapped_futures).await
706    }
707
708    fn add_tool_returns(
709        &mut self,
710        returns: Vec<(String, Option<String>, Result<ToolReturn, ToolError>)>,
711    ) -> Result<(), AgentRunError> {
712        // CRITICAL: First add the previous response as a model response part.
713        // This ensures proper user/assistant alternation for Anthropic and other providers.
714        // Without this, we'd send consecutive user messages which violates the API contract.
715        if let Some(last_response) = self.state.responses.last() {
716            let mut response_req = ModelRequest::new();
717            response_req
718                .parts
719                .push(ModelRequestPart::ModelResponse(Box::new(
720                    last_response.clone(),
721                )));
722            self.state.messages.push(response_req);
723        }
724
725        let mut req = ModelRequest::new();
726
727        for (tool_name, tool_call_id, result) in returns {
728            match result {
729                Ok(ret) => {
730                    let mut part = ToolReturnPart::new(&tool_name, ret.content);
731                    if let Some(id) = tool_call_id {
732                        part = part.with_tool_call_id(id);
733                    }
734                    req.parts.push(ModelRequestPart::ToolReturn(part));
735                }
736                Err(e) => {
737                    let mut part = RetryPromptPart::new(format!("Tool error: {}", e));
738                    part = part.with_tool_name(&tool_name);
739                    if let Some(id) = tool_call_id {
740                        part = part.with_tool_call_id(id);
741                    }
742                    req.parts.push(ModelRequestPart::RetryPrompt(part));
743                }
744            }
745        }
746
747        if !req.parts.is_empty() {
748            self.state.messages.push(req);
749        }
750
751        Ok(())
752    }
753
754    fn add_retry_message(&mut self, error: OutputValidationError) -> Result<(), AgentRunError> {
755        let mut req = ModelRequest::new();
756        let part = RetryPromptPart::new(error.retry_message());
757        req.parts.push(ModelRequestPart::RetryPrompt(part));
758        self.state.messages.push(req);
759        Ok(())
760    }
761
762    async fn validate_output(&self, output: Output) -> Result<Output, OutputValidationError> {
763        let mut output = output;
764        for validator in &self.agent.output_validators {
765            output = validator.validate(output, &self.ctx).await?;
766        }
767        Ok(output)
768    }
769
770    fn finalize(self) -> Result<AgentRunResult<Output>, AgentRunError> {
771        let output = self.state.final_output.ok_or(AgentRunError::NoOutput)?;
772
773        Ok(AgentRunResult {
774            output,
775            messages: self.state.messages,
776            responses: self.state.responses,
777            usage: self.state.usage,
778            run_id: self.state.run_id,
779            finish_reason: self.state.finish_reason.unwrap_or(FinishReason::Stop),
780            metadata: self.ctx.metadata.clone(),
781        })
782    }
783
784    /// Get current messages.
785    pub fn messages(&self) -> &[ModelRequest] {
786        &self.state.messages
787    }
788
789    /// Get current usage.
790    pub fn usage(&self) -> &RunUsage {
791        &self.state.usage
792    }
793
794    /// Get run ID.
795    pub fn run_id(&self) -> &str {
796        &self.state.run_id
797    }
798
799    /// Check if finished.
800    pub fn is_finished(&self) -> bool {
801        self.state.finished
802    }
803
804    /// Get current step number.
805    pub fn step_number(&self) -> u32 {
806        self.state.step
807    }
808
809    /// Cancel the running agent.
810    ///
811    /// If this run was created with cancellation support via
812    /// [`AgentRun::new_with_cancel`], this will trigger cancellation.
813    /// The next call to `step()` will return `AgentRunError::Cancelled`.
814    ///
815    /// If this run was created without cancellation support (via `new`),
816    /// this method does nothing.
817    pub fn cancel(&self) {
818        if let Some(ref token) = self.cancel_token {
819            token.cancel();
820        }
821    }
822
823    /// Check if this run was cancelled.
824    ///
825    /// Returns `true` if a cancellation token was provided and it has been
826    /// triggered, `false` otherwise.
827    pub fn is_cancelled(&self) -> bool {
828        self.cancel_token
829            .as_ref()
830            .map(|t| t.is_cancelled())
831            .unwrap_or(false)
832    }
833
834    /// Get the cancellation token if one was provided.
835    ///
836    /// This can be used to share the token with other tasks that need
837    /// to coordinate cancellation.
838    pub fn cancellation_token(&self) -> Option<&CancellationToken> {
839        self.cancel_token.as_ref()
840    }
841}
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn test_run_options_default() {
849        let options = RunOptions::default();
850        assert!(options.model_settings.is_none());
851        assert!(options.message_history.is_none());
852    }
853
854    #[test]
855    fn test_run_options_builder() {
856        let options = RunOptions::new()
857            .model_settings(ModelSettings::new().temperature(0.5))
858            .metadata(serde_json::json!({"key": "value"}));
859
860        assert!(options.model_settings.is_some());
861        assert!(options.metadata.is_some());
862    }
863
864    #[test]
865    fn test_step_result_eq() {
866        assert_eq!(StepResult::Continue, StepResult::Continue);
867        assert_eq!(StepResult::ToolsExecuted(2), StepResult::ToolsExecuted(2));
868        assert_ne!(StepResult::ToolsExecuted(1), StepResult::ToolsExecuted(2));
869    }
870
871    #[test]
872    fn test_canonicalize_tool_call_args_in_response_converts_string_args_to_json() {
873        let mut response = ModelResponse::new();
874        response.add_part(ModelResponsePart::ToolCall(
875            serdes_ai_core::messages::ToolCallPart::new(
876                "demo_tool",
877                ToolCallArgs::string("{foo: bar,}"),
878            )
879            .with_tool_call_id("call_1"),
880        ));
881
882        canonicalize_tool_call_args_in_response(&mut response);
883
884        match &response.parts[0] {
885            ModelResponsePart::ToolCall(tc) => {
886                assert!(matches!(tc.args, ToolCallArgs::Json(_)));
887            }
888            _ => panic!("expected tool call part"),
889        }
890    }
891}