Skip to content

Commit 8190578

Browse files
committed
Error type refactor; renamed GHA jobs
1 parent 6b08ba7 commit 8190578

File tree

8 files changed

+100
-62
lines changed

8 files changed

+100
-62
lines changed

.github/workflows/rust-lint-test.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Lint & Test
1+
name: test
22

33
on:
44
push:
@@ -23,13 +23,13 @@ defaults:
2323
working-directory: ./rust
2424

2525
jobs:
26-
build-and-test:
26+
rust:
2727
strategy:
2828
fail-fast: false
2929
matrix:
3030
os: [ ubuntu-latest, macos-latest, windows-latest ]
3131

32-
name: Test multiple workspaces on ${{ matrix.os }}
32+
name: Rust SDK Tests [${{ matrix.os }}]
3333
runs-on: ${{ matrix.os }}
3434

3535
env:
@@ -49,8 +49,11 @@ jobs:
4949
- name: Check clippy
5050
run: cargo clippy -- -D warnings
5151

52-
- name: Publish dry-run
52+
- name: Publish ag-ui-core dry-run
5353
run: cargo publish -p ag-ui-core --dry-run
5454

55+
- name: Publish ag-ui-client dry-run
56+
run: cargo publish -p ag-ui-client --dry-run
57+
5558
- name: Run tests
5659
run: cargo test --verbose

sdks/community/rust/crates/ag-ui-client/src/agent.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use futures::stream::StreamExt;
22
use std::collections::HashSet;
3-
use thiserror::Error;
43

54
use crate::core::JsonValue;
65
use crate::core::types::{
@@ -132,18 +131,7 @@ impl<StateT> Default for AgentStateMutation<StateT> {
132131
}
133132

134133
// Error types
135-
#[derive(Error, Debug)]
136-
pub enum AgentError {
137-
#[error("Agent execution failed: {message}")]
138-
ExecutionError { message: String },
139-
#[error("Invalid configuration: {message}")]
140-
ConfigError { message: String },
141-
#[error("Serialization error: {source}")]
142-
SerializationError {
143-
#[from]
144-
source: serde_json::Error,
145-
},
146-
}
134+
pub use crate::error::AgUiClientError as AgentError;
147135

148136
// TODO: Expand documentation
149137
/// Agent trait
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
#[non_exhaustive]
5+
pub enum AgUiClientError {
6+
// Configuration/usage errors
7+
#[error("Invalid configuration: {message}")]
8+
Config { message: String },
9+
10+
// Transport-level HTTP failures from reqwest
11+
#[error("HTTP transport error: {0}")]
12+
HttpTransport(#[from] reqwest::Error),
13+
14+
// Non-success HTTP status with body snippet
15+
#[error("HTTP status {status}: {context}")]
16+
HttpStatus {
17+
status: reqwest::StatusCode,
18+
context: String,
19+
},
20+
21+
// SSE parsing/framing/UTF-8 errors
22+
#[error("SSE parse error: {message}")]
23+
SseParse { message: String },
24+
25+
// JSON serialization/deserialization
26+
#[error("JSON error: {0}")]
27+
Json(#[from] serde_json::Error),
28+
29+
// Errors from subscribers/callbacks
30+
#[error("Subscriber error: {message}")]
31+
Subscriber { message: String },
32+
33+
// Pipeline catch-all
34+
#[error("Agent execution error: {message}")]
35+
Execution { message: String },
36+
}
37+
38+
impl AgUiClientError {
39+
pub fn config(m: impl Into<String>) -> Self {
40+
Self::Config { message: m.into() }
41+
}
42+
pub fn exec(m: impl Into<String>) -> Self {
43+
Self::Execution { message: m.into() }
44+
}
45+
46+
pub fn is_retryable(&self) -> bool {
47+
use reqwest::StatusCode as S;
48+
match self {
49+
AgUiClientError::HttpTransport(e) => e.is_connect() || e.is_timeout() || e.is_request(),
50+
AgUiClientError::HttpStatus { status, .. } => {
51+
status.is_server_error() || *status == S::TOO_MANY_REQUESTS
52+
}
53+
_ => false,
54+
}
55+
}
56+
57+
pub fn is_user_input(&self) -> bool {
58+
matches!(self, AgUiClientError::Config { .. })
59+
}
60+
}
61+
62+
pub type Result<T> = std::result::Result<T, AgUiClientError>;

sdks/community/rust/crates/ag-ui-client/src/event_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ where
358358
serde_json::from_value(serde_json::to_value(e.delta.clone())?)?;
359359

360360
json_patch::patch(&mut state_val, &patches).map_err(|err| {
361-
AgentError::ExecutionError {
361+
AgentError::Execution {
362362
message: format!("Failed to apply state patch: {err}"),
363363
}
364364
})?;

sdks/community/rust/crates/ag-ui-client/src/http.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::str::FromStr;
77

88
use crate::Agent;
99
use crate::agent::AgentError;
10-
use crate::agent::AgentError::SerializationError;
1110
use crate::core::event::Event;
1211
use crate::core::types::RunAgentInput;
1312
use crate::core::{AgentState, FwdProps};
@@ -61,7 +60,7 @@ impl HttpAgentBuilder {
6160

6261
/// Set the base URL from a string, returning Result for validation
6362
pub fn with_url_str(mut self, url: &str) -> Result<Self, AgentError> {
64-
let parsed_url = Url::parse(url).map_err(|e| AgentError::ConfigError {
63+
let parsed_url = Url::parse(url).map_err(|e| AgentError::Config {
6564
message: format!("Invalid URL '{url}': {e}"),
6665
})?;
6766
self.base_url = Some(parsed_url);
@@ -76,10 +75,10 @@ impl HttpAgentBuilder {
7675

7776
/// Add a single header by name and value strings
7877
pub fn with_header(mut self, name: &str, value: &str) -> Result<Self, AgentError> {
79-
let header_name = HeaderName::from_str(name).map_err(|e| AgentError::ConfigError {
78+
let header_name = HeaderName::from_str(name).map_err(|e| AgentError::Config {
8079
message: format!("Invalid header name '{value}': {e}"),
8180
})?;
82-
let header_value = HeaderValue::from_str(value).map_err(|e| AgentError::ConfigError {
81+
let header_value = HeaderValue::from_str(value).map_err(|e| AgentError::Config {
8382
message: format!("Invalid header value '{value}': {e}"),
8483
})?;
8584
self.header_map.insert(header_name, header_value);
@@ -115,13 +114,13 @@ impl HttpAgentBuilder {
115114
}
116115

117116
pub fn build(self) -> Result<HttpAgent, AgentError> {
118-
let base_url = self.base_url.ok_or(AgentError::ConfigError {
117+
let base_url = self.base_url.ok_or(AgentError::Config {
119118
message: "Base URL is required".to_string(),
120119
})?;
121120

122121
// Validate URL scheme
123122
if !["http", "https"].contains(&base_url.scheme()) {
124-
return Err(AgentError::ConfigError {
123+
return Err(AgentError::Config {
125124
message: format!("Unsupported URL scheme: {}", base_url.scheme()),
126125
});
127126
}
@@ -142,14 +141,6 @@ impl Default for HttpAgentBuilder {
142141
}
143142
}
144143

145-
impl From<reqwest::Error> for AgentError {
146-
fn from(err: reqwest::Error) -> Self {
147-
AgentError::ExecutionError {
148-
message: err.to_string(),
149-
}
150-
}
151-
}
152-
153144
#[async_trait]
154145
impl<StateT: AgentState, FwdPropsT: FwdProps> Agent<StateT, FwdPropsT> for HttpAgent {
155146
async fn run(
@@ -165,6 +156,17 @@ impl<StateT: AgentState, FwdPropsT: FwdProps> Agent<StateT, FwdPropsT> for HttpA
165156
.send()
166157
.await?;
167158

159+
// Check HTTP status and surface structured error on non-success
160+
let status = response.status();
161+
if !status.is_success() {
162+
let text = response.text().await.unwrap_or_default();
163+
let snippet: String = text.chars().take(512).collect();
164+
return Err(AgentError::HttpStatus {
165+
status,
166+
context: snippet,
167+
});
168+
}
169+
168170
// Convert the response to an SSE event stream
169171
let stream = response
170172
.event_source()
@@ -173,15 +175,12 @@ impl<StateT: AgentState, FwdPropsT: FwdProps> Agent<StateT, FwdPropsT> for HttpA
173175
Ok(event) => {
174176
trace!("Received event: {event:?}");
175177

176-
let event_data: Event<StateT> = serde_json::from_str(&event.data)
177-
.map_err(|err| SerializationError { source: err })?;
178+
let event_data: Event<StateT> = serde_json::from_str(&event.data)?;
178179
debug!("Deserialized event: {event_data:?}");
179180

180181
Ok(event_data)
181182
}
182-
Err(err) => Err(AgentError::ExecutionError {
183-
message: err.to_string(),
184-
}),
183+
Err(err) => Err(err),
185184
})
186185
.boxed();
187186
Ok(stream)

sdks/community/rust/crates/ag-ui-client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![doc = include_str!("../README.md")]
22

33
pub mod agent;
4+
pub mod error;
45
pub mod event_handler;
56
pub mod http;
67
pub mod sse;

sdks/community/rust/crates/ag-ui-client/src/sse.rs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,8 @@
1+
use crate::error::AgUiClientError;
12
use bytes::Bytes;
23
use futures::{Stream, StreamExt};
34
use reqwest::Response;
45
use std::future::Future;
5-
use thiserror::Error;
6-
7-
/// Error type for SSE response processing
8-
#[derive(Error, Debug)]
9-
pub enum SseError {
10-
/// Error when parsing SSE data
11-
#[error("Failed to parse SSE data: {0}")]
12-
ParseError(String),
13-
14-
/// Error from reqwest
15-
#[error("HTTP error: {0}")]
16-
HttpError(#[from] reqwest::Error),
17-
18-
/// Error when deserializing JSON
19-
#[error("JSON deserialization error: {0}")]
20-
JsonError(#[from] serde_json::Error),
21-
}
226

237
/// Represents a parsed Server-Sent Event
248
#[derive(Debug)]
@@ -61,14 +45,14 @@ pub trait SseResponseExt {
6145
/// Converts a reqwest::Response into a Stream of SSE events
6246
fn event_source(
6347
self,
64-
) -> impl Future<Output = impl Stream<Item = Result<SseEvent, SseError>>> + Send;
48+
) -> impl Future<Output = impl Stream<Item = Result<SseEvent, AgUiClientError>>> + Send;
6549
}
6650

6751
impl SseResponseExt for Response {
6852
#[allow(clippy::manual_async_fn)]
6953
fn event_source(
7054
self,
71-
) -> impl Future<Output = impl Stream<Item = Result<SseEvent, SseError>>> + Send {
55+
) -> impl Future<Output = impl Stream<Item = Result<SseEvent, AgUiClientError>>> + Send {
7256
async move {
7357
// Create a stream of bytes from the response
7458
let stream = self.bytes_stream();
@@ -87,7 +71,7 @@ impl SseEventProcessor {
8771
#[allow(clippy::new_ret_no_self)]
8872
fn new(
8973
stream: impl Stream<Item = Result<Bytes, reqwest::Error>> + 'static,
90-
) -> impl Stream<Item = Result<SseEvent, SseError>> {
74+
) -> impl Stream<Item = Result<SseEvent, AgUiClientError>> {
9175
let mut buffer = String::new();
9276

9377
// Process the stream
@@ -96,7 +80,7 @@ impl SseEventProcessor {
9680
// Map reqwest errors
9781
let chunk = match chunk_result {
9882
Ok(chunk) => chunk,
99-
Err(err) => return vec![Err(SseError::HttpError(err))],
83+
Err(err) => return vec![Err(AgUiClientError::HttpTransport(err))],
10084
};
10185

10286
// Convert bytes to string and append to buffer
@@ -110,7 +94,9 @@ impl SseEventProcessor {
11094

11195
events
11296
}
113-
Err(e) => vec![Err(SseError::ParseError(format!("Invalid UTF-8: {e}")))],
97+
Err(e) => vec![Err(AgUiClientError::SseParse {
98+
message: format!("Invalid UTF-8: {e}"),
99+
})],
114100
}
115101
})
116102
.flat_map(futures::stream::iter)
@@ -122,7 +108,7 @@ impl SseEventProcessor {
122108
/// Returns a tuple of (events, new_buffer) where:
123109
/// - events: A vector of parsed events or errors
124110
/// - new_buffer: The remaining buffer that might contain incomplete events
125-
fn process_raw_sse_events(buffer: &str) -> (Vec<Result<SseEvent, SseError>>, String) {
111+
fn process_raw_sse_events(buffer: &str) -> (Vec<Result<SseEvent, AgUiClientError>>, String) {
126112
let mut results = Vec::new();
127113
let chunks: Vec<&str> = buffer.split("\n\n").collect();
128114

@@ -159,7 +145,7 @@ fn process_raw_sse_events(buffer: &str) -> (Vec<Result<SseEvent, SseError>>, Str
159145
}
160146

161147
/// Parse a single SSE event text into an SseEvent
162-
fn parse_sse_event(event_text: &str) -> Result<SseEvent, SseError> {
148+
fn parse_sse_event(event_text: &str) -> Result<SseEvent, AgUiClientError> {
163149
let mut event = None;
164150
let mut id = None;
165151
let mut data_lines = Vec::new();

sdks/community/rust/crates/ag-ui-core/src/event.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use crate::types::{Message, Role};
44
use crate::types::{MessageId, RunId, ThreadId, ToolCallId};
55
use serde::{Deserialize, Serialize};
66

7-
/// Event types for AG-UI protocol
87
/// Event types for AG-UI protocol
98
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]

0 commit comments

Comments
 (0)