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