use reqwest::header::{HeaderName, HeaderValue}; pub const SESSION_ID_HEADER: &str = "agent-session-id"; pub const TOOL_CALL_REQUEST_ID_HEADER: &str = "agent-tool-call-request-id"; pub const WORKING_DIR_HEADER: &str = "agent-working-dir"; tokio::task_local! { pub static SESSION_ID: Option; } pub async fn with_session_id(session_id: Option, f: F) -> F::Output where F: std::future::Future, { SESSION_ID.scope(session_id, f).await } pub fn current_session_id() -> Option { SESSION_ID.try_with(|id| id.clone()).ok().flatten() } pub fn session_id_request_builder() -> goose_providers::api_client::RequestBuilderDecorator { std::sync::Arc::new(|request| { let (client, request) = request.build_split(); let mut request = request?; let session_header = HeaderName::from_static(SESSION_ID_HEADER); request.headers_mut().remove(&session_header); if let Some(session_id) = current_session_id() { let value = HeaderValue::from_str(&session_id)?; request.headers_mut().insert(session_header, value); } Ok(reqwest::RequestBuilder::from_parts(client, request)) }) } /// Local OS user running goose, shared by the OTLP `user.name` resource /// attribute and the `session.user` span attribute so the two never drift. pub fn session_user() -> String { std::env::var("USER") .or_else(|_| std::env::var("LOGNAME")) .unwrap_or_else(|_| "unknown".to_string()) } /// Hostname of the machine running goose, shared by the OTLP `host.name` /// resource attribute and the `session.host` span attribute. pub fn session_host() -> String { gethostname::gethostname().to_string_lossy().to_string() } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_session_id_available_when_set() { with_session_id(Some("test-session-123".to_string()), async { assert_eq!(current_session_id(), Some("test-session-123".to_string())); }) .await; } #[tokio::test] async fn test_session_id_none_when_not_set() { let id = current_session_id(); assert_eq!(id, None); } #[tokio::test] async fn test_session_id_none_when_explicitly_none() { with_session_id(None, async { assert_eq!(current_session_id(), None); }) .await; } #[tokio::test] async fn test_session_id_none_clears_outer_scope() { with_session_id(Some("outer-session".to_string()), async { assert_eq!(current_session_id(), Some("outer-session".to_string())); with_session_id(None, async { assert_eq!(current_session_id(), None); }) .await; assert_eq!(current_session_id(), Some("outer-session".to_string())); }) .await; } #[tokio::test] async fn test_session_id_scoped_correctly() { assert_eq!(current_session_id(), None); with_session_id(Some("outer-session".to_string()), async { assert_eq!(current_session_id(), Some("outer-session".to_string())); with_session_id(Some("inner-session".to_string()), async { assert_eq!(current_session_id(), Some("inner-session".to_string())); }) .await; assert_eq!(current_session_id(), Some("outer-session".to_string())); }) .await; assert_eq!(current_session_id(), None); } #[tokio::test] async fn test_session_id_across_await_points() { with_session_id(Some("persistent-session".to_string()), async { assert_eq!(current_session_id(), Some("persistent-session".to_string())); tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; assert_eq!(current_session_id(), Some("persistent-session".to_string())); }) .await; } }