Skip to main content

tau_agent_base/
lib.rs

1//! Shared types, wire protocol, and utilities for the tau agent workspace.
2//!
3//! This is the leaf crate that every other tau workspace crate depends on.
4//! Dependencies are kept minimal: serde, serde_json, thiserror, and futures
5//! (for async JSON-line I/O helpers used by the client and plugin crates).
6
7pub mod config_chain;
8pub mod model_resolve;
9pub mod paths;
10pub mod plugin_protocol;
11pub mod plugin_service;
12pub mod project;
13pub mod protocol;
14pub mod subscription_usage;
15pub mod tool_prompt;
16pub mod types;
17pub mod usage_totals;
18
19pub use types::*;
20pub use usage_totals::UsageTotals;
21
22// ---------------------------------------------------------------------------
23// JSON-line I/O helpers (sync only — async helpers live in dependent crates)
24// ---------------------------------------------------------------------------
25
26/// Serialize `val` as a single JSON line and flush the writer (sync).
27pub fn write_json_line<T: serde::Serialize>(
28    writer: &mut impl std::io::Write,
29    val: &T,
30) -> Result<()> {
31    let mut line = serde_json::to_string(val).map_err(|e| Error::Io(e.to_string()))?;
32    line.push('\n');
33    writer
34        .write_all(line.as_bytes())
35        .map_err(|e| Error::Io(e.to_string()))?;
36    writer.flush().map_err(|e| Error::Io(e.to_string()))?;
37    Ok(())
38}
39
40/// Read a single JSON line (sync).  Returns `Ok(None)` on EOF.
41pub fn read_json_line<T: serde::de::DeserializeOwned>(
42    reader: &mut impl std::io::BufRead,
43) -> Result<Option<T>> {
44    let mut line = String::new();
45    let n = reader
46        .read_line(&mut line)
47        .map_err(|e| Error::Io(e.to_string()))?;
48    if n == 0 {
49        return Ok(None);
50    }
51    let val = serde_json::from_str(&line).map_err(|e| Error::Parse(e.to_string()))?;
52    Ok(Some(val))
53}
54
55// ---------------------------------------------------------------------------
56// Async JSON-line I/O helpers
57// ---------------------------------------------------------------------------
58
59/// Serialize `val` as a single JSON line and flush the writer (async).
60pub async fn write_json_line_async<T: serde::Serialize>(
61    writer: &mut (impl futures::io::AsyncWrite + Unpin),
62    val: &T,
63) -> Result<()> {
64    use futures::io::AsyncWriteExt;
65    let mut line = serde_json::to_string(val).map_err(|e| Error::Io(e.to_string()))?;
66    line.push('\n');
67    writer
68        .write_all(line.as_bytes())
69        .await
70        .map_err(|e| Error::Io(e.to_string()))?;
71    writer.flush().await.map_err(|e| Error::Io(e.to_string()))?;
72    Ok(())
73}
74
75/// Read a single JSON line (async).  Returns `Ok(None)` on EOF.
76pub async fn read_json_line_async<T: serde::de::DeserializeOwned>(
77    reader: &mut (impl futures::io::AsyncBufRead + Unpin),
78) -> Result<Option<T>> {
79    use futures::io::AsyncBufReadExt;
80    let mut line = String::new();
81    let n = reader
82        .read_line(&mut line)
83        .await
84        .map_err(|e| Error::Io(e.to_string()))?;
85    if n == 0 {
86        return Ok(None);
87    }
88    let val = serde_json::from_str(&line).map_err(|e| Error::Parse(e.to_string()))?;
89    Ok(Some(val))
90}
91
92// ---------------------------------------------------------------------------
93// String utilities
94// ---------------------------------------------------------------------------
95
96/// Truncate `s` to at most `max_bytes` bytes, rounding down to a char boundary.
97pub fn truncate_str(s: &str, max_bytes: usize) -> &str {
98    if s.len() <= max_bytes {
99        return s;
100    }
101    let mut end = max_bytes;
102    while end > 0 && !s.is_char_boundary(end) {
103        end -= 1;
104    }
105    &s[..end]
106}
107
108/// Truncate `s` to at most `max_bytes` bytes from the *end*, rounding up to a
109/// char boundary.
110pub fn truncate_str_end(s: &str, max_bytes: usize) -> &str {
111    if s.len() <= max_bytes {
112        return s;
113    }
114    let mut start = s.len() - max_bytes;
115    while start < s.len() && !s.is_char_boundary(start) {
116        start += 1;
117    }
118    &s[start..]
119}
120
121// ---------------------------------------------------------------------------
122// Error type
123// ---------------------------------------------------------------------------
124
125#[derive(Debug, thiserror::Error)]
126pub enum Error {
127    #[error("no provider registered for api: {0}")]
128    NoProvider(String),
129    #[error("no API key for provider: {0}")]
130    NoApiKey(String),
131    #[error("HTTP error: {0}")]
132    Http(String),
133    #[error("HTTP {status}: {message}")]
134    HttpStatus {
135        status: u16,
136        message: String,
137        /// Retry-After header value in seconds, if present.
138        retry_after: Option<u64>,
139    },
140    #[error("parse error: {0}")]
141    Parse(String),
142    #[error("IO error: {0}")]
143    Io(String),
144    #[error("channel closed")]
145    ChannelClosed,
146    #[error("cancelled")]
147    Cancelled,
148    #[error("provider throttled until {0}")]
149    Throttled(String),
150}
151
152pub type Result<T> = std::result::Result<T, Error>;
153
154// ---------------------------------------------------------------------------
155// Shared test utilities
156// ---------------------------------------------------------------------------
157
158/// Crate-wide env-var mutex.
159///
160/// Multiple test modules (`config_chain`, `project`, …) mutate `XDG_CONFIG_HOME` /
161/// `HOME`. Each module's tests must hold this lock before touching the env,
162/// otherwise parallel test threads stomp on each other.
163#[cfg(test)]
164pub(crate) static TEST_ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());