Skip to main content

ras_agent/application/
run_agent.rs

1use std::sync::Arc;
2
3use ras_cdp::BrowserPort;
4use ras_dom::DomExtractor;
5use ras_errors::AppError;
6use ras_events::EventBus;
7use ras_llm::{ChatMessage, LlmClient};
8use ras_tools::domain::registry::ActionRegistry;
9use ras_types::{AgentId, StepId, TargetId};
10
11use crate::application::render_step_message::render_step_message;
12use crate::application::run_step::RunStep;
13use crate::domain::agent_history::{AgentHistory, AgentHistoryList, StepRecord};
14use crate::domain::loop_detector::ActionLoopDetector;
15
16const AGENT_OUTPUT_SHAPE: &str = r#"{"current_state":{"evaluation_previous_goal":"...","memory":"...","next_goal":"..."},"action":[{"name":"<action_name>","parameters":{...}}]}"#;
17
18pub struct RunAgent {
19    pub agent: AgentId,
20    pub task: String,
21    pub max_steps: u32,
22    pub bound_target: Option<TargetId>,
23    pub primary_llm: Arc<dyn LlmClient>,
24    pub fallback_llm: Option<Arc<dyn LlmClient>>,
25    pub registry: Arc<ActionRegistry>,
26    pub browser: Arc<dyn BrowserPort>,
27    pub events: Arc<dyn EventBus>,
28    pub dom_extractor: Option<Arc<dyn DomExtractor>>,
29}
30
31impl RunAgent {
32    pub fn new(
33        task: impl Into<String>,
34        llm: Arc<dyn LlmClient>,
35        registry: Arc<ActionRegistry>,
36        browser: Arc<dyn BrowserPort>,
37        events: Arc<dyn EventBus>,
38    ) -> Self {
39        Self {
40            agent: AgentId::new(),
41            task: task.into(),
42            max_steps: 25,
43            bound_target: None,
44            primary_llm: llm,
45            fallback_llm: None,
46            registry,
47            browser,
48            events,
49            dom_extractor: None,
50        }
51    }
52
53    #[must_use]
54    pub fn with_target(mut self, target: TargetId) -> Self {
55        self.bound_target = Some(target);
56        self
57    }
58
59    #[must_use]
60    pub fn with_dom_extractor(mut self, extractor: Arc<dyn DomExtractor>) -> Self {
61        self.dom_extractor = Some(extractor);
62        self
63    }
64
65    #[must_use]
66    pub fn with_fallback(mut self, fallback: Arc<dyn LlmClient>) -> Self {
67        self.fallback_llm = Some(fallback);
68        self
69    }
70
71    #[must_use]
72    pub fn with_max_steps(mut self, max_steps: u32) -> Self {
73        self.max_steps = max_steps;
74        self
75    }
76
77    pub async fn execute(self) -> Result<AgentHistoryList, AppError> {
78        let runner = RunStep::new(
79            self.primary_llm.clone(),
80            self.fallback_llm.clone(),
81            self.registry.clone(),
82            self.browser.clone(),
83            self.events.clone(),
84            self.dom_extractor.clone(),
85            self.bound_target.clone(),
86        );
87        let mut detector = ActionLoopDetector::new();
88        let mut history = AgentHistory {
89            agent: self.agent,
90            task: self.task.clone(),
91            steps: Vec::new(),
92        };
93        let mut last_step_ms: Option<u64> = None;
94        let mut empty_streak: u32 = 0;
95        for n in 0..self.max_steps {
96            let prompt = build_prompt(&self.task, &history.steps, &self.registry);
97            let mut record = runner
98                .execute(StepId(n), self.max_steps, prompt, &mut detector)
99                .await?;
100            record.metadata.step_interval_ms = last_step_ms;
101            last_step_ms = Some(record.metadata.duration_ms);
102
103            let done = record.results.iter().any(|r| r.is_done);
104            if record.output.action.is_empty() {
105                empty_streak += 1;
106                tracing::warn!(
107                    step = n,
108                    "model returned empty action list (streak={empty_streak}); treating as stalled"
109                );
110            } else {
111                empty_streak = 0;
112            }
113
114            history.steps.push(record);
115            if done {
116                break;
117            }
118            if empty_streak >= 2 {
119                tracing::error!("agent stalled: 2 consecutive empty action lists, aborting");
120                break;
121            }
122        }
123        let mut list = AgentHistoryList::default();
124        list.push(history);
125        Ok(list)
126    }
127}
128
129fn build_prompt(task: &str, history: &[StepRecord], registry: &ActionRegistry) -> Vec<ChatMessage> {
130    let catalog = render_action_catalog(registry);
131    let system = format!(
132        "You are a browsing agent. Your task: {task}\n\n\
133         You drive a real browser via the actions listed below. Each step you must \
134         emit ONE JSON object matching this exact shape (no prose, no markdown fences):\n\
135         {AGENT_OUTPUT_SHAPE}\n\n\
136         Available actions:\n{catalog}\n\n\
137         Rules:\n\
138         - Use only action names from the catalog above. Match parameters_schema exactly.\n\
139         - Plan one or two atomic steps at a time; the runtime executes them in order \
140           and feeds results back on the next turn.\n\
141         - Call the `done` action when the task is complete; pass the final answer in \
142           its parameters. Returning an empty action list is treated as a failure."
143    );
144    let mut out = vec![ChatMessage::system(system)];
145    for step in history.iter().rev().take(4).rev() {
146        if let Ok(j) = serde_json::to_string(&step.output) {
147            out.push(ChatMessage::assistant_text(j));
148        }
149        if let Some(msg) = render_step_message(step) {
150            out.push(msg);
151        }
152    }
153    out.push(ChatMessage::user_text(
154        "Decide the next action(s). Respond with the JSON object only.",
155    ));
156    out
157}
158
159fn render_action_catalog(registry: &ActionRegistry) -> String {
160    let mut buf = String::new();
161    for (name, reg) in registry.iter() {
162        let schema =
163            serde_json::to_string(&reg.metadata.parameters_schema).unwrap_or_else(|_| "{}".into());
164        buf.push_str("- ");
165        buf.push_str(&name.0);
166        buf.push_str(": ");
167        buf.push_str(&reg.metadata.description);
168        if reg.metadata.terminates_sequence {
169            buf.push_str(" [terminates step]");
170        }
171        buf.push_str("\n  parameters_schema: ");
172        buf.push_str(&schema);
173        buf.push('\n');
174    }
175    if buf.is_empty() {
176        buf.push_str("(no actions registered)\n");
177    }
178    buf
179}