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