Skip to main content

ras_agent/application/
run_step.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use chrono::Utc;
5use ras_cdp::BrowserPort;
6use ras_errors::AppError;
7use ras_events::EventBus;
8use ras_llm::{ChatMessage, ChatResponse, InvokeOptions, LlmClient};
9use ras_tools::domain::registry::{ActionRegistry, ToolContext};
10use ras_types::{ActionResult, StepId};
11use url::Url;
12
13use crate::application::compute_action_hash::compute_action_hash;
14use crate::application::detect_loop::{build_budget_warning, build_loop_nudge};
15use crate::application::fallback_llm::should_switch_to_fallback;
16use crate::application::parse_output::parse_agent_output;
17use crate::domain::agent_history::StepRecord;
18use crate::domain::loop_detector::ActionLoopDetector;
19use crate::domain::step_metadata::StepMetadata;
20
21pub struct RunStep {
22    primary_llm: Arc<dyn LlmClient>,
23    fallback_llm: Option<Arc<dyn LlmClient>>,
24    registry: Arc<ActionRegistry>,
25    browser: Arc<dyn BrowserPort>,
26    events: Arc<dyn EventBus>,
27}
28
29impl RunStep {
30    #[must_use]
31    pub fn new(
32        primary: Arc<dyn LlmClient>,
33        fallback: Option<Arc<dyn LlmClient>>,
34        registry: Arc<ActionRegistry>,
35        browser: Arc<dyn BrowserPort>,
36        events: Arc<dyn EventBus>,
37    ) -> Self {
38        Self {
39            primary_llm: primary,
40            fallback_llm: fallback,
41            registry,
42            browser,
43            events,
44        }
45    }
46
47    pub async fn execute(
48        &self,
49        step: StepId,
50        max_steps: u32,
51        prompt: Vec<ChatMessage>,
52        detector: &mut ActionLoopDetector,
53    ) -> Result<StepRecord, AppError> {
54        let started = Instant::now();
55        let mut messages = prompt;
56        if let Some(nudge) = build_loop_nudge(detector) {
57            messages.push(nudge);
58        }
59        if let Some(warn) = build_budget_warning(step.0, max_steps) {
60            messages.push(warn);
61        }
62        let response = self.invoke_with_fallback(messages).await?;
63        let output = parse_agent_output(&response)?;
64
65        let target = self.browser.focused_target().await.ok();
66        let page_url = match &target {
67            Some(t) => self
68                .browser
69                .evaluate(t, "location.href")
70                .await
71                .ok()
72                .and_then(|v| v.as_str().and_then(|s| Url::parse(s).ok())),
73            None => None,
74        };
75
76        let mut results = Vec::new();
77        for action in &output.action {
78            detector.record_action(compute_action_hash(action));
79            let Some(reg) = self.registry.get(&action.name) else {
80                results.push(ActionResult::err(format!(
81                    "unknown action: {}",
82                    action.name.0
83                )));
84                break;
85            };
86            let ctx = ToolContext {
87                browser: self.browser.clone(),
88                events: self.events.clone(),
89                page_url: page_url.clone(),
90                available_files: Vec::new(),
91            };
92            match reg.handler.execute(action.parameters.clone(), ctx).await {
93                Ok(r) => {
94                    let terminates = reg.metadata.terminates_sequence;
95                    let is_done = r.is_done;
96                    let is_err = r.is_error();
97                    results.push(r);
98                    if terminates || is_done || is_err {
99                        break;
100                    }
101                }
102                Err(e) => {
103                    results.push(ActionResult::err(e.to_string()));
104                    break;
105                }
106            }
107        }
108
109        let metadata = StepMetadata {
110            duration_ms: started.elapsed().as_millis() as u64,
111            step_interval_ms: None,
112            usage: response.usage,
113            model: Some(response.model.clone()),
114            fallback_used: false,
115        };
116        Ok(StepRecord {
117            step,
118            started_at: Utc::now(),
119            url: page_url,
120            output,
121            results,
122            metadata,
123        })
124    }
125
126    async fn invoke_with_fallback(
127        &self,
128        messages: Vec<ChatMessage>,
129    ) -> Result<ChatResponse, AppError> {
130        let opts = InvokeOptions::default();
131        match self
132            .primary_llm
133            .ainvoke(messages.clone(), opts.clone())
134            .await
135        {
136            Ok(r) => Ok(r),
137            Err(e) if should_switch_to_fallback(&e) => match &self.fallback_llm {
138                Some(fb) => fb.ainvoke(messages, opts).await,
139                None => Err(e),
140            },
141            Err(e) => Err(e),
142        }
143    }
144}
145
146#[must_use]
147pub fn done_result(text: impl Into<String>) -> ActionResult {
148    ActionResult::done(text)
149}