websearch/types.rs
1//! Types for the web-search layer.
2
3use serde::{Deserialize, Serialize};
4
5/// The slim reference entry shared with the fetch path.
6pub use crate::providers::Provider;
7pub use crate::refs::Reference;
8pub use crate::tls::TlsConfig;
9
10/// A single search hit, carrying its reference index so the inline body can
11/// cite `[N]` while the full URL lives in the reference block.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct SearchResult {
14 pub title: String,
15 pub snippet: String,
16 pub url: String,
17 pub ref_index: usize,
18}
19
20/// Whether a search actually answered.
21///
22/// Without this, a bot-challenge page (served with HTTP 200 and no result rows)
23/// and a query with genuinely no hits were the same observable outcome: zero
24/// results and a success exit. Callers could not tell "the web has no answer"
25/// from "I was refused", and agents reliably concluded the former.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum SearchStatus {
29 /// Results were parsed.
30 Ok,
31 /// A real results page that reported no hits.
32 Empty,
33 /// A challenge, rate-limit, or otherwise unparseable page. A failure.
34 Blocked,
35}
36
37impl SearchStatus {
38 /// Did the search fail to answer? Drives the CLI exit code and the MCP
39 /// `isError` flag.
40 pub fn is_failure(self) -> bool {
41 matches!(self, SearchStatus::Blocked)
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SearchOutput {
47 pub query: String,
48 pub results: Vec<SearchResult>,
49 pub references: Vec<Reference>,
50 pub token_estimate: usize,
51 pub result_count: usize,
52 /// Whether the search answered — see [`SearchStatus`].
53 pub status: SearchStatus,
54 /// Which backend produced these results, so a silent fallback from a keyed
55 /// provider to scraped DuckDuckGo is visible to the caller.
56 pub provider: String,
57}
58
59#[derive(Debug, Clone, Deserialize)]
60pub struct SearchOptions {
61 pub query: String,
62 pub max_results: Option<usize>,
63 pub safe_search: Option<bool>,
64 pub timeout_secs: u64,
65 /// TLS trust configuration (OS store is honoured by default; this carries
66 /// the explicit `--ca-cert` / `--insecure` overrides).
67 #[serde(default)]
68 pub tls: TlsConfig,
69 /// Which backend to query. Defaults to keyless DuckDuckGo Lite so the tool
70 /// needs no configuration; credentials are resolved by the caller (the CLI
71 /// reads them from flags, the environment, or the config file) and passed
72 /// in already populated — the library never reads a config file itself.
73 #[serde(default)]
74 pub provider: Provider,
75 /// Backend to try when the primary errors or is blocked.
76 #[serde(default)]
77 pub fallback: Option<Provider>,
78}
79
80impl Default for SearchOptions {
81 fn default() -> Self {
82 Self {
83 query: String::new(),
84 max_results: Some(5),
85 safe_search: None,
86 timeout_secs: 10,
87 tls: TlsConfig::default(),
88 provider: Provider::default(),
89 fallback: None,
90 }
91 }
92}