refactor: decouple modules, add resilience middleware and state bus [skip-regression-check]

Break circular dependencies between agent, db, channels, and context
modules by extracting shared domain types to neutral locations:

- Extract routine types to src/models/routine.rs
- Extract ToolFailureRecord to src/models/tool_failure.rs
- Move SseEvent to src/events.rs as DomainEvent
- Move HttpInterceptor to src/observability/
- Move truncate_preview to src/util.rs

Add generic resilience middleware (src/resilience/):
- ErrorClassifier, RetryLayer, CircuitBreakerLayer, HealthTracker

Add state invalidation bus (src/state_bus.rs)
Add boundary chaos tests (tests/boundary_chaos.rs)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-03-14 21:17:25 -07:00
co-authored by Claude Opus 4.6
parent 757d24bd90
commit b04d14b114
27 changed files with 2398 additions and 1061 deletions
+5 -43
View File
@@ -51,32 +51,10 @@ pub struct MemorySnapshotEntry {
pub content: String,
}
/// A recorded HTTP request/response pair.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpExchange {
pub request: HttpExchangeRequest,
pub response: HttpExchangeResponse,
}
/// The request side of an HTTP exchange.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpExchangeRequest {
pub method: String,
pub url: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub headers: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
}
/// The response side of an HTTP exchange.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpExchangeResponse {
pub status: u16,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub headers: Vec<(String, String)>,
pub body: String,
}
// Re-export HTTP exchange types from their canonical location in observability.
pub use crate::observability::http_interceptor::{
HttpExchange, HttpExchangeRequest, HttpExchangeResponse, HttpInterceptor,
};
/// A single step in the trace.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -144,23 +122,7 @@ pub struct ExpectedToolResult {
pub content: String,
}
// ── HTTP interceptor ───────────────────────────────────────────────
/// Trait for intercepting HTTP requests from tools.
///
/// During recording, the interceptor captures exchanges after the real
/// request completes. During replay, it short-circuits with a recorded response.
#[async_trait]
pub trait HttpInterceptor: Send + Sync + std::fmt::Debug {
/// Called before making an HTTP request.
///
/// Return `Some(response)` to short-circuit (replay mode).
/// Return `None` to let the real request proceed (recording mode).
async fn before_request(&self, request: &HttpExchangeRequest) -> Option<HttpExchangeResponse>;
/// Called after a real HTTP request completes (recording mode only).
async fn after_response(&self, request: &HttpExchangeRequest, response: &HttpExchangeResponse);
}
// ── HTTP interceptor impls ─────────────────────────────────────────
/// Records HTTP exchanges during a live session.
#[derive(Debug)]