From 99b965b7f0111b4b561ad9ae65d7e1e433dfe7c0 Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 30 May 2026 04:32:38 +0700 Subject: [PATCH 1/2] feat(dom): name clickables from text, onclick, icon class, and role Previously the LLM received clickables as bare `[N] tag`, named only when an aria-label/alt/title/name/placeholder/value attribute was present. Anchor text, JS click handlers, and icon-only buttons arrived anonymous, leaving the screenshot as the sole disambiguator. - Detection unions the attribute heuristic with CDP's native isClickable set, catching JS-attached listeners, not just markup. - ax_name falls back through: aria/alt/title/name/placeholder -> descendant text -> onclick handler name (humanized, verb only) -> FontAwesome icon class -> role. - role overrides the displayed tag, so renders as button. - clickable_map: raise cap 80 -> 200 and sort visible-first so on-screen elements survive truncation. - Split snapshot_parser into snapshot_parser/clickables/clickable_naming to satisfy the 200-LOC module cap. Verified live against headless Chrome via the new (ignored) e2e test clickable_names_resolve_text_onclick_icon_and_role. Co-Authored-By: Claude Opus 4.8 (1M context) --- ras-agent/src/application/clickable_map.rs | 28 ++- .../chromiumoxide/clickable_naming.rs | 197 ++++++++++++++++++ .../chromiumoxide/clickables.rs | 146 +++++++++++++ .../src/infrastructure/chromiumoxide/mod.rs | 2 + .../chromiumoxide/snapshot_parser.rs | 80 +------ ras-tools/tests/e2e_grounding.rs | 62 ++++++ 6 files changed, 436 insertions(+), 79 deletions(-) create mode 100644 ras-dom/src/infrastructure/chromiumoxide/clickable_naming.rs create mode 100644 ras-dom/src/infrastructure/chromiumoxide/clickables.rs diff --git a/ras-agent/src/application/clickable_map.rs b/ras-agent/src/application/clickable_map.rs index e3feedf..0b22824 100644 --- a/ras-agent/src/application/clickable_map.rs +++ b/ras-agent/src/application/clickable_map.rs @@ -1,14 +1,18 @@ -use ras_dom::BrowserStateSummary; +use ras_dom::{BrowserStateSummary, ClickableElement}; -const CLICKABLE_LIMIT: usize = 80; +const CLICKABLE_LIMIT: usize = 200; const NAME_BUDGET: usize = 80; pub(crate) fn render_clickable_map(summary: &BrowserStateSummary) -> String { if summary.clickables.is_empty() { return String::new(); } + let mut ordered: Vec<&_> = summary.clickables.iter().collect(); + let visible = |c: &&ClickableElement| c.bbox.width > 0.0 && c.bbox.height > 0.0; + ordered.sort_by_key(|c| !visible(c)); + let mut buf = String::from("clickable_elements:\n"); - for c in summary.clickables.iter().take(CLICKABLE_LIMIT) { + for c in ordered.iter().take(CLICKABLE_LIMIT) { buf.push_str(&format!(" [{}] {}", c.index, c.tag)); if let Some(name) = &c.ax_name { buf.push_str(&format!(" \"{}\"", truncate(name, NAME_BUDGET))); @@ -101,6 +105,24 @@ mod tests { assert!(!out.contains('"')); } + #[test] + fn visible_elements_survive_truncation_over_hidden() { + let mut many: Vec = (0..(CLICKABLE_LIMIT as u32)) + .map(|i| click(i, "div", None, None)) + .collect(); + for c in &mut many { + c.bbox.width = 0.0; + c.bbox.height = 0.0; + } + let mut visible = click(999, "button", Some("Save"), None); + visible.bbox.width = 10.0; + visible.bbox.height = 10.0; + many.push(visible); + let out = render_clickable_map(&summary_with(many)); + assert!(out.contains("[999] button \"Save\"")); + assert!(out.contains("…and 1 more (truncated)")); + } + #[test] fn truncates_beyond_limit_with_more_marker() { let many: Vec = (0..(CLICKABLE_LIMIT as u32 + 5)) diff --git a/ras-dom/src/infrastructure/chromiumoxide/clickable_naming.rs b/ras-dom/src/infrastructure/chromiumoxide/clickable_naming.rs new file mode 100644 index 0000000..24a1dc0 --- /dev/null +++ b/ras-dom/src/infrastructure/chromiumoxide/clickable_naming.rs @@ -0,0 +1,197 @@ +pub(crate) fn derive_ax_name(attrs: &[(String, String)]) -> Option { + for key in ["aria-label", "alt", "title", "name", "placeholder"] { + if let Some(v) = attrs.iter().find(|(k, _)| k == key) + && !v.1.is_empty() + { + return Some(v.1.clone()); + } + } + None +} + +pub(crate) fn derive_label(attrs: &[(String, String)]) -> Option { + attrs + .iter() + .find(|(k, _)| k == "value") + .map(|(_, v)| v.clone()) +} + +pub(crate) fn role_value(attrs: &[(String, String)]) -> Option { + attrs + .iter() + .find(|(k, _)| k == "role") + .map(|(_, v)| v.clone()) + .filter(|v| !v.is_empty()) +} + +/// `onclick="viewBankAccount('0')"` -> "view bank account". Verb only; args dropped. +pub(crate) fn onclick_name(attrs: &[(String, String)]) -> Option { + let handler = attrs.iter().find(|(k, _)| k == "onclick")?; + let fname: String = handler + .1 + .trim_start() + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$') + .collect(); + if matches!( + fname.as_str(), + "" | "return" | "function" | "javascript" | "window" | "this" | "void" | "if" | "event" + ) { + return None; + } + let name = humanize(&fname); + (!name.is_empty()).then_some(name) +} + +/// FontAwesome icon class -> a verb, e.g. `fa-eye` -> "view", `fa-chart-line` -> "chart line". +pub(crate) fn icon_name(attrs: &[(String, String)]) -> Option { + let class = attrs.iter().find(|(k, _)| k == "class")?; + for tok in class.1.split_whitespace() { + let Some(icon) = tok.strip_prefix("fa-") else { + continue; + }; + if icon.is_empty() || is_fa_style_token(icon) { + continue; + } + let mapped = match icon { + "eye" => "view", + "eye-slash" => "hide", + "plus" | "plus-circle" | "plus-square" => "add", + "minus" | "minus-circle" => "remove", + "trash" | "trash-alt" | "trash-can" => "delete", + "pen" | "pencil" | "pencil-alt" | "pen-to-square" | "edit" => "edit", + "times" | "xmark" | "close" => "close", + "check" | "check-circle" => "confirm", + "search" | "magnifying-glass" => "search", + "download" => "download", + "upload" => "upload", + "cog" | "cogs" | "gear" => "settings", + "copy" => "copy", + "save" | "floppy-disk" => "save", + "print" => "print", + "sync" | "rotate" | "arrows-rotate" => "refresh", + "lock" => "lock", + "unlock" => "unlock", + _ => return Some(humanize(icon)), + }; + return Some(mapped.to_string()); + } + None +} + +fn is_fa_style_token(icon: &str) -> bool { + matches!( + icon, + "fw" | "spin" + | "pulse" + | "lg" + | "sm" + | "xs" + | "2x" + | "3x" + | "4x" + | "5x" + | "stack" + | "stack-1x" + | "stack-2x" + | "inverse" + | "border" + | "pull-left" + | "pull-right" + | "rotate-90" + | "rotate-180" + | "rotate-270" + | "flip-horizontal" + | "flip-vertical" + ) +} + +pub(crate) fn collapse_ws(s: &str) -> String { + s.split_whitespace().collect::>().join(" ") +} + +/// Split camelCase / snake_case / kebab-case into lowercase words. +pub(crate) fn humanize(s: &str) -> String { + let mut out = String::new(); + let mut prev_alnum = false; + for ch in s.chars() { + if ch == '_' || ch == '-' || ch == '$' { + if prev_alnum { + out.push(' '); + } + prev_alnum = false; + continue; + } + if ch.is_uppercase() && prev_alnum { + out.push(' '); + } + out.extend(ch.to_lowercase()); + prev_alnum = ch.is_alphanumeric(); + } + out.split_whitespace().collect::>().join(" ") +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pairs(kv: &[(&str, &str)]) -> Vec<(String, String)> { + kv.iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + } + + #[test] + fn humanize_splits_camel_snake_kebab() { + assert_eq!(humanize("viewBankAccount"), "view bank account"); + assert_eq!(humanize("addBankAccountModal"), "add bank account modal"); + assert_eq!(humanize("chart-line"), "chart line"); + } + + #[test] + fn onclick_name_takes_handler_verb_only() { + let a = pairs(&[("onclick", "viewBankAccount('0')")]); + assert_eq!(onclick_name(&a).as_deref(), Some("view bank account")); + } + + #[test] + fn onclick_name_skips_js_keywords() { + assert_eq!(onclick_name(&pairs(&[("onclick", "return false;")])), None); + assert_eq!(onclick_name(&pairs(&[("onclick", "")])), None); + } + + #[test] + fn icon_name_maps_known_and_humanizes_unknown() { + let eye = icon_name(&pairs(&[("class", "fas fa-eye text-secondary")])); + assert_eq!(eye.as_deref(), Some("view")); + let add = icon_name(&pairs(&[("class", "fas fa-plus-circle")])); + assert_eq!(add.as_deref(), Some("add")); + let chart = icon_name(&pairs(&[("class", "fas fa-chart-line")])); + assert_eq!(chart.as_deref(), Some("chart line")); + } + + #[test] + fn icon_name_ignores_style_tokens() { + assert_eq!(icon_name(&pairs(&[("class", "fa-fw fa-2x")])), None); + } + + #[test] + fn role_value_reads_non_empty_role() { + assert_eq!( + role_value(&pairs(&[("role", "button")])).as_deref(), + Some("button") + ); + assert_eq!(role_value(&pairs(&[("role", "")])), None); + } + + #[test] + fn aria_label_wins_over_handlers() { + let a = pairs(&[("aria-label", "Close dialog"), ("onclick", "shutModal()")]); + assert_eq!(derive_ax_name(&a).as_deref(), Some("Close dialog")); + } + + #[test] + fn collapse_ws_normalizes() { + assert_eq!(collapse_ws(" Deposit \n 14 "), "Deposit 14"); + } +} diff --git a/ras-dom/src/infrastructure/chromiumoxide/clickables.rs b/ras-dom/src/infrastructure/chromiumoxide/clickables.rs new file mode 100644 index 0000000..9b30aea --- /dev/null +++ b/ras-dom/src/infrastructure/chromiumoxide/clickables.rs @@ -0,0 +1,146 @@ +use std::collections::HashMap; + +use chromiumoxide::cdp::browser_protocol::dom_snapshot::{ + DocumentSnapshot, NodeTreeSnapshot, StringIndex, +}; +use ras_types::BackendNodeId; + +use crate::domain::clickable::ClickableElement; +use crate::domain::node::BoundingBox; +use crate::infrastructure::chromiumoxide::clickable_naming::{ + collapse_ws, derive_ax_name, derive_label, icon_name, onclick_name, role_value, +}; +use crate::infrastructure::chromiumoxide::snapshot_parser::{ + decode_attrs, is_clickable, lookup_index, +}; + +const TEXT_CLIMB_DEPTH: usize = 6; +const TEXT_NODE_TYPE: i64 = 3; + +pub(crate) fn extract_clickables( + doc: &DocumentSnapshot, + strings: &[String], + layout_bbox: &HashMap, +) -> Vec { + let nodes = &doc.nodes; + let Some(node_names) = &nodes.node_name else { + return Vec::new(); + }; + let backend_ids = nodes.backend_node_id.as_ref(); + let attrs = nodes.attributes.as_ref(); + let n = node_names.len(); + + let empty: Vec = Vec::new(); + let tags: Vec = node_names + .iter() + .map(|name| lookup_index(strings, *name.inner()).to_lowercase()) + .collect(); + let attr_pairs: Vec> = (0..n) + .map(|i| { + let idxs = attrs + .and_then(|a| a.get(i)) + .map(|a| a.inner().as_slice()) + .unwrap_or(&empty); + decode_attrs(idxs, strings) + }) + .collect(); + + let mut clickable = vec![false; n]; + if let Some(rb) = &nodes.is_clickable { + for idx in &rb.index { + if *idx >= 0 && (*idx as usize) < n { + clickable[*idx as usize] = true; + } + } + } + for i in 0..n { + clickable[i] = clickable[i] || is_clickable(&tags[i], &attr_pairs[i]); + } + + let text_map = build_text_map(nodes, strings, &clickable, n); + + let mut out = Vec::new(); + let mut clickable_index: u32 = 0; + for i in 0..n { + if !clickable[i] { + continue; + } + let pairs = &attr_pairs[i]; + let backend_node_id = backend_ids + .and_then(|b| b.get(i)) + .map(|v| BackendNodeId(*v.inner())) + .unwrap_or(BackendNodeId(0)); + let bbox = layout_bbox + .get(&(i as i64)) + .copied() + .unwrap_or(BoundingBox { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, + }); + let ax_name = derive_ax_name(pairs) + .or_else(|| text_map.get(&i).cloned()) + .or_else(|| onclick_name(pairs)) + .or_else(|| icon_name(pairs)) + .or_else(|| role_value(pairs)); + let label = derive_label(pairs); + let tag = role_value(pairs).unwrap_or_else(|| tags[i].clone()); + out.push(ClickableElement { + index: clickable_index, + backend_node_id, + bbox, + xpath: format!("//*[backendNodeId={}]", backend_node_id.0), + stable_hash: String::new(), + ax_name, + tag, + label, + }); + clickable_index += 1; + } + out +} + +/// Join visible text descendants to their nearest clickable ancestor, so anchors like +/// `Home` surface "Home" even though the text lives in a child node. +fn build_text_map( + nodes: &NodeTreeSnapshot, + strings: &[String], + clickable: &[bool], + n: usize, +) -> HashMap { + let mut map: HashMap = HashMap::new(); + let (Some(parents), Some(types), Some(values)) = + (&nodes.parent_index, &nodes.node_type, &nodes.node_value) + else { + return map; + }; + for i in 0..n { + if types.get(i).copied() != Some(TEXT_NODE_TYPE) { + continue; + } + let raw = values.get(i).map(|v| lookup_index(strings, *v.inner())); + let text = raw.as_deref().map(str::trim).unwrap_or(""); + if text.is_empty() { + continue; + } + let mut cur = i; + for _ in 0..TEXT_CLIMB_DEPTH { + let parent = parents.get(cur).copied().unwrap_or(-1); + if parent < 0 || parent as usize >= n { + break; + } + let parent = parent as usize; + if clickable[parent] { + let entry = map.entry(parent).or_default(); + if !entry.is_empty() { + entry.push(' '); + } + entry.push_str(&collapse_ws(text)); + break; + } + cur = parent; + } + } + map +} diff --git a/ras-dom/src/infrastructure/chromiumoxide/mod.rs b/ras-dom/src/infrastructure/chromiumoxide/mod.rs index 602d741..225c0ee 100644 --- a/ras-dom/src/infrastructure/chromiumoxide/mod.rs +++ b/ras-dom/src/infrastructure/chromiumoxide/mod.rs @@ -1,3 +1,5 @@ +pub mod clickable_naming; +pub mod clickables; pub mod extractor; pub mod highlight; pub mod snapshot; diff --git a/ras-dom/src/infrastructure/chromiumoxide/snapshot_parser.rs b/ras-dom/src/infrastructure/chromiumoxide/snapshot_parser.rs index 879568b..cebb545 100644 --- a/ras-dom/src/infrastructure/chromiumoxide/snapshot_parser.rs +++ b/ras-dom/src/infrastructure/chromiumoxide/snapshot_parser.rs @@ -1,12 +1,12 @@ use chromiumoxide::cdp::browser_protocol::dom_snapshot::{ CaptureSnapshotReturns, DocumentSnapshot, NodeTreeSnapshot, StringIndex, }; -use ras_types::BackendNodeId; use url::Url; use crate::domain::clickable::ClickableElement; use crate::domain::node::BoundingBox; use crate::domain::state_summary::PageStatistics; +use crate::infrastructure::chromiumoxide::clickables::extract_clickables; const CLICKABLE_TAGS: &[&str] = &[ "a", "button", "input", "select", "textarea", "summary", "label", "details", @@ -33,7 +33,7 @@ pub(crate) fn parse_snapshot( Ok((url, title, clickables, page_stats)) } -fn lookup_index(strings: &[String], idx: i64) -> String { +pub(crate) fn lookup_index(strings: &[String], idx: i64) -> String { if idx >= 0 && (idx as usize) < strings.len() { strings[idx as usize].clone() } else { @@ -63,61 +63,7 @@ fn build_layout_index(doc: &DocumentSnapshot) -> std::collections::HashMap, -) -> Vec { - let nodes = &doc.nodes; - let Some(node_names) = &nodes.node_name else { - return Vec::new(); - }; - let backend_ids = nodes.backend_node_id.as_ref(); - let attrs = nodes.attributes.as_ref(); - let mut out = Vec::new(); - let mut clickable_index: u32 = 0; - for (i, name) in node_names.iter().enumerate() { - let tag_lower = lookup_index(strings, *name.inner()).to_lowercase(); - let empty: Vec = Vec::new(); - let attr_idxs: &[StringIndex] = attrs - .and_then(|a| a.get(i)) - .map(|a| a.inner().as_slice()) - .unwrap_or(&empty); - let attr_pairs = decode_attrs(attr_idxs, strings); - if !is_clickable(&tag_lower, &attr_pairs) { - continue; - } - let backend_node_id = backend_ids - .and_then(|b| b.get(i)) - .map(|v| BackendNodeId(*v.inner())) - .unwrap_or(BackendNodeId(0)); - let bbox = layout_bbox - .get(&(i as i64)) - .copied() - .unwrap_or(BoundingBox { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, - }); - let ax_name = derive_ax_name(&attr_pairs); - let label = derive_label(&attr_pairs); - out.push(ClickableElement { - index: clickable_index, - backend_node_id, - bbox, - xpath: format!("//*[backendNodeId={}]", backend_node_id.0), - stable_hash: String::new(), - ax_name, - tag: tag_lower, - label, - }); - clickable_index += 1; - } - out -} - -fn decode_attrs(attr_idxs: &[StringIndex], strings: &[String]) -> Vec<(String, String)> { +pub(crate) fn decode_attrs(attr_idxs: &[StringIndex], strings: &[String]) -> Vec<(String, String)> { let mut pairs = Vec::with_capacity(attr_idxs.len() / 2); let mut i = 0; while i + 1 < attr_idxs.len() { @@ -130,7 +76,7 @@ fn decode_attrs(attr_idxs: &[StringIndex], strings: &[String]) -> Vec<(String, S pairs } -fn is_clickable(tag: &str, attrs: &[(String, String)]) -> bool { +pub(crate) fn is_clickable(tag: &str, attrs: &[(String, String)]) -> bool { if CLICKABLE_TAGS.contains(&tag) { return true; } @@ -145,24 +91,6 @@ fn is_clickable(tag: &str, attrs: &[(String, String)]) -> bool { false } -fn derive_ax_name(attrs: &[(String, String)]) -> Option { - for key in ["aria-label", "alt", "title", "name", "placeholder"] { - if let Some(v) = attrs.iter().find(|(k, _)| k == key) - && !v.1.is_empty() - { - return Some(v.1.clone()); - } - } - None -} - -fn derive_label(attrs: &[(String, String)]) -> Option { - attrs - .iter() - .find(|(k, _)| k == "value") - .map(|(_, v)| v.clone()) -} - fn build_page_stats(nodes: &NodeTreeSnapshot, clickables: &[ClickableElement]) -> PageStatistics { let total = nodes.node_name.as_ref().map(|v| v.len()).unwrap_or(0) as u32; let visible = clickables diff --git a/ras-tools/tests/e2e_grounding.rs b/ras-tools/tests/e2e_grounding.rs index 9b502c3..38c16ce 100644 --- a/ras-tools/tests/e2e_grounding.rs +++ b/ras-tools/tests/e2e_grounding.rs @@ -107,3 +107,65 @@ async fn click_and_type_resolve_through_clickable_map() { "button click must trigger onclick (got {title:?})" ); } + +/// Live check that the clickable map names elements from descendant text, onclick handlers, +/// and FontAwesome icon classes, and that `role` overrides the displayed tag. +#[tokio::test] +#[ignore] +async fn clickable_names_resolve_text_onclick_icon_and_role() { + let url = cdp_url().expect(SETUP_HINT); + let adapter = ChromiumoxideAdapter::connect(url, Duration::from_secs(30)) + .await + .expect("connect"); + let browser_arc = adapter.browser_arc(); + let browser: Arc = Arc::new(adapter); + let extractor: Arc = Arc::new(ChromiumoxideDomExtractor::new( + browser_arc, + Duration::from_secs(30), + )); + + let html = concat!( + "data:text/html,", + "", + "", + "", + ); + let target = browser + .create_target(&html.parse().expect("data url")) + .await + .expect("create target"); + tokio::time::sleep(Duration::from_millis(700)).await; + + let summary = extractor.snapshot(&target).await.expect("snapshot"); + let names: Vec<(String, Option)> = summary + .clickables + .iter() + .map(|c| (c.tag.clone(), c.ax_name.clone())) + .collect(); + let has = |name: &str| { + summary + .clickables + .iter() + .any(|c| c.ax_name.as_deref() == Some(name)) + }; + + // 1. anchor text — direct child and nested . + assert!(has("Dashboard"), "direct anchor text; got {names:?}"); + assert!(has("Home"), "nested span text; got {names:?}"); + + // 2. onclick handler name wins, and role=button overrides the tag. + let eye = summary + .clickables + .iter() + .find(|c| c.ax_name.as_deref() == Some("view bank account")) + .unwrap_or_else(|| panic!("onclick-derived name; got {names:?}")); + assert_eq!(eye.tag, "button", "role overrides tag; got {names:?}"); + assert!( + has("add bank account modal"), + "second handler; got {names:?}" + ); +} From 5aeea9cbb1da36ae8c953ba996748442f4885cc3 Mon Sep 17 00:00:00 2001 From: KeyCode17 Date: Sat, 30 May 2026 04:37:12 +0700 Subject: [PATCH 2/2] chore: bump to 3.1.0 --- Cargo.lock | 76 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2ac78b..a1b9471 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1465,7 +1465,7 @@ dependencies = [ [[package]] name = "ras-agent" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "chrono", @@ -1497,7 +1497,7 @@ dependencies = [ [[package]] name = "ras-browser" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-cdp", @@ -1515,7 +1515,7 @@ dependencies = [ [[package]] name = "ras-cdp" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1534,7 +1534,7 @@ dependencies = [ [[package]] name = "ras-cli" -version = "3.0.0" +version = "3.1.0" dependencies = [ "anyhow", "clap", @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "ras-cloud" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1581,7 +1581,7 @@ dependencies = [ [[package]] name = "ras-config" -version = "3.0.0" +version = "3.1.0" dependencies = [ "dotenvy", "once_cell", @@ -1595,7 +1595,7 @@ dependencies = [ [[package]] name = "ras-cosmium" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-cdp", @@ -1612,7 +1612,7 @@ dependencies = [ [[package]] name = "ras-daemon" -version = "3.0.0" +version = "3.1.0" dependencies = [ "anyhow", "dotenvy", @@ -1632,7 +1632,7 @@ dependencies = [ [[package]] name = "ras-dom" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "chromiumoxide", @@ -1652,7 +1652,7 @@ dependencies = [ [[package]] name = "ras-errors" -version = "3.0.0" +version = "3.1.0" dependencies = [ "serde", "thiserror", @@ -1660,7 +1660,7 @@ dependencies = [ [[package]] name = "ras-events" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-broadcast", "async-trait", @@ -1678,7 +1678,7 @@ dependencies = [ [[package]] name = "ras-filesystem" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1693,7 +1693,7 @@ dependencies = [ [[package]] name = "ras-judge" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "image", @@ -1708,7 +1708,7 @@ dependencies = [ [[package]] name = "ras-llm" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1722,7 +1722,7 @@ dependencies = [ [[package]] name = "ras-llm-anthropic" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "chrono", @@ -1745,7 +1745,7 @@ dependencies = [ [[package]] name = "ras-llm-bedrock" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1762,7 +1762,7 @@ dependencies = [ [[package]] name = "ras-llm-cerebras" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1780,7 +1780,7 @@ dependencies = [ [[package]] name = "ras-llm-cloud" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1797,7 +1797,7 @@ dependencies = [ [[package]] name = "ras-llm-deepseek" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1815,7 +1815,7 @@ dependencies = [ [[package]] name = "ras-llm-google" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1832,7 +1832,7 @@ dependencies = [ [[package]] name = "ras-llm-groq" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1850,7 +1850,7 @@ dependencies = [ [[package]] name = "ras-llm-langchain" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1867,7 +1867,7 @@ dependencies = [ [[package]] name = "ras-llm-mistral" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1885,7 +1885,7 @@ dependencies = [ [[package]] name = "ras-llm-oci" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1902,7 +1902,7 @@ dependencies = [ [[package]] name = "ras-llm-ollama" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1919,7 +1919,7 @@ dependencies = [ [[package]] name = "ras-llm-openai" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1936,7 +1936,7 @@ dependencies = [ [[package]] name = "ras-llm-openrouter" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1954,7 +1954,7 @@ dependencies = [ [[package]] name = "ras-llm-vercel" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1972,7 +1972,7 @@ dependencies = [ [[package]] name = "ras-mcp" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -1989,7 +1989,7 @@ dependencies = [ [[package]] name = "ras-recording" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "image", @@ -2004,7 +2004,7 @@ dependencies = [ [[package]] name = "ras-sandbox" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -2017,7 +2017,7 @@ dependencies = [ [[package]] name = "ras-skills" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -2034,7 +2034,7 @@ dependencies = [ [[package]] name = "ras-telemetry" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -2048,7 +2048,7 @@ dependencies = [ [[package]] name = "ras-tokens" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "chrono", @@ -2066,7 +2066,7 @@ dependencies = [ [[package]] name = "ras-tools" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "indexmap", @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "ras-types" -version = "3.0.0" +version = "3.1.0" dependencies = [ "chrono", "indexmap", @@ -2103,7 +2103,7 @@ dependencies = [ [[package]] name = "ras-validation" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-errors", @@ -2117,7 +2117,7 @@ dependencies = [ [[package]] name = "ras-watchdogs" -version = "3.0.0" +version = "3.1.0" dependencies = [ "async-trait", "ras-browser", diff --git a/Cargo.toml b/Cargo.toml index 05a2ea3..3118313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ exclude = ["examples", "tests", "docs", "scripts"] default-members = ["ras-cli", "ras-daemon"] [workspace.package] -version = "3.0.0" +version = "3.1.0" edition = "2024" rust-version = "1.95.0" license = "MIT"