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