ras_agent/application/
run_agent.rs1use 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(®.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(®.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}