use std::any::TypeId;
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, SystemTime};
use rmcp::ServerHandler;
use crate::config::Options;
#[cfg(test)]
use crate::config::env::MapEnv;
#[cfg(not(test))]
use crate::config::env::ProcessEnv;
use crate::config::env::{AGENTCAT_PROJECT_ID, Env, resolve_api_base_url, sdk_enabled};
use crate::custom::{CustomEvent, publish_string};
#[cfg(feature = "test-util")]
use crate::delivery::Observer;
use crate::delivery::http::ApiPublisher;
use crate::delivery::{Delivery, DeliveryConfig, LineWriter, Publisher};
use crate::error::Error;
use crate::event::CallRecord;
use crate::event::assemble::EventJob;
use crate::logging;
use crate::registry::ToolRegistry;
use crate::sink::Sink;
use crate::tracked::Tracked;
#[cfg(test)]
mod tests;
#[derive(Clone)]
pub struct AgentCat {
inner: Arc<Inner>,
}
impl fmt::Debug for AgentCat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.debug_fields(f, "AgentCat")
}
}
pub(crate) struct Inner {
pub(crate) project_id: String,
pub(crate) options: Options,
pub(crate) enabled: bool,
pub(crate) sink: Arc<dyn Sink>,
registries: Mutex<HashMap<TypeId, ToolRegistry>>,
}
impl fmt::Debug for Inner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.debug_fields(f, "Inner")
}
}
impl Inner {
fn debug_fields(&self, f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
f.debug_struct(name)
.field("project_id", &self.project_id)
.field("options", &self.options)
.finish_non_exhaustive()
}
pub(crate) fn with_registry<H: 'static, R>(&self, f: impl FnOnce(&mut ToolRegistry) -> R) -> R {
let mut registries = self
.registries
.lock()
.unwrap_or_else(PoisonError::into_inner);
f(registries.entry(TypeId::of::<H>()).or_default())
}
}
const NO_PROJECT_WARNING: &str = "Warning: No project_id provided (AgentCat::from_env reads it from AGENTCAT_PROJECT_ID). Events will not be sent anywhere.";
struct Transport {
publisher: Arc<dyn Publisher>,
startup_lines: Vec<String>,
write: Option<LineWriter>,
}
impl Transport {
#[cfg(not(test))]
fn process(options: &Options, env: &dyn Env) -> Self {
Self::api(options, env)
}
fn api(options: &Options, env: &dyn Env) -> Self {
logging::configure(options.log_to(), env.home_dir());
let mut startup_lines = Vec::new();
let publisher = ApiPublisher::new(resolve_api_base_url(options, env), &mut startup_lines);
Self {
publisher: Arc::new(publisher),
startup_lines,
write: None,
}
}
#[cfg(test)]
fn process(options: &Options, env: &dyn Env) -> Self {
let _ = (options, env);
Self {
publisher: Arc::new(OfflinePublisher),
startup_lines: Vec::new(),
write: None,
}
}
}
#[cfg(any(test, feature = "test-util"))]
struct OfflinePublisher;
#[cfg(any(test, feature = "test-util"))]
impl Publisher for OfflinePublisher {
fn publish(
&self,
_request: agentcat_api::models::PublishEventRequest,
) -> crate::sink::BoxFuture<'static, Result<(), String>> {
Box::pin(std::future::ready(Ok(())))
}
}
#[cfg(not(test))]
fn default_env() -> ProcessEnv {
ProcessEnv
}
#[cfg(test)]
fn default_env() -> MapEnv {
MapEnv::new(&[])
}
struct DisabledSink;
impl Sink for DisabledSink {
fn log(&self, _message: String) {}
fn record(&self, _record: CallRecord) {}
fn event(&self, _job: EventJob) {}
}
#[cfg(test)]
fn delivery_sink(
project_id: &str,
options: &Options,
publisher: Arc<dyn Publisher>,
config: DeliveryConfig,
) -> Arc<dyn Sink> {
Arc::new(Delivery::new(
project_id.to_owned(),
options,
publisher,
None,
config,
))
}
const _: () = {
const fn send_sync_static<T: Send + Sync + 'static>() {}
send_sync_static::<AgentCat>();
};
impl AgentCat {
pub fn new(project_id: impl Into<String>, options: Options) -> Self {
Self::build(project_id.into(), options, &default_env())
}
pub fn from_env(options: Options) -> Self {
Self::from_env_in(options, &default_env())
}
fn from_env_in(options: Options, env: &dyn Env) -> Self {
let project_id = env.var(AGENTCAT_PROJECT_ID).unwrap_or_default();
Self::build(project_id, options, env)
}
#[cfg(test)]
pub(crate) fn with_env(project_id: &str, options: Options, env: &dyn Env) -> Self {
Self::build(project_id.to_owned(), options, env)
}
#[cfg(test)]
pub(crate) fn with_api_transport(project_id: &str, options: Options, env: &dyn Env) -> Self {
Self::build_with(project_id.to_owned(), options, env, Transport::api)
}
fn build(project_id: String, options: Options, env: &dyn Env) -> Self {
Self::build_with(project_id, options, env, Transport::process)
}
fn build_with(
project_id: String,
options: Options,
env: &dyn Env,
transport: fn(&Options, &dyn Env) -> Transport,
) -> Self {
if !sdk_enabled(env) {
return Self::from_inner(Inner {
project_id,
options,
enabled: false,
sink: Arc::new(DisabledSink),
registries: Mutex::default(),
});
}
let transport = transport(&options, env);
Self::assemble(project_id, options, transport, DeliveryConfig::default())
}
fn assemble(
project_id: String,
options: Options,
transport: Transport,
config: DeliveryConfig,
) -> Self {
let project = project_id.clone();
let delivery = match transport.write {
None => Delivery::new(project, &options, transport.publisher, None, config),
Some(write) => {
Delivery::with_writer(project, &options, transport.publisher, None, config, write)
}
};
if project_id.is_empty() {
delivery.log(NO_PROJECT_WARNING.to_owned());
}
for line in transport.startup_lines {
delivery.log(line);
}
Self::from_inner(Inner {
project_id,
options,
enabled: true,
sink: Arc::new(delivery),
registries: Mutex::default(),
})
}
#[cfg(test)]
pub(crate) fn with_delivery(
project_id: &str,
options: Options,
publisher: Arc<dyn Publisher>,
config: DeliveryConfig,
) -> Self {
let sink = delivery_sink(project_id, &options, publisher, config);
Self::with_sink(project_id, options, sink)
}
#[cfg(feature = "test-util")]
pub(crate) fn offline_observed(
project_id: String,
options: Options,
observer: Arc<dyn Observer>,
) -> Self {
let delivery = Delivery::new(
project_id.clone(),
&options,
Arc::new(OfflinePublisher),
Some(observer),
DeliveryConfig::default(),
);
Self::from_inner(Inner {
project_id,
options,
enabled: true,
sink: Arc::new(delivery),
registries: Mutex::default(),
})
}
#[cfg(test)]
pub(crate) fn with_sink(project_id: &str, options: Options, sink: Arc<dyn Sink>) -> Self {
Self::from_inner(Inner {
project_id: project_id.to_owned(),
options,
enabled: true,
sink,
registries: Mutex::default(),
})
}
#[cfg(test)]
pub(crate) fn disabled_with_sink(
project_id: &str,
options: Options,
sink: Arc<dyn Sink>,
) -> Self {
Self::from_inner(Inner {
project_id: project_id.to_owned(),
options,
enabled: false,
sink,
registries: Mutex::default(),
})
}
fn from_inner(inner: Inner) -> Self {
Self {
inner: Arc::new(inner),
}
}
pub fn track<H: ServerHandler>(&self, handler: H) -> Tracked<H> {
Tracked::new(handler, self.clone())
}
pub fn publish_custom_event(
&self,
session_id: &str,
project_id: &str,
event: CustomEvent,
) -> Result<(), Error> {
publish_string(self, session_id, project_id, event, SystemTime::now())
}
pub(crate) fn inner(&self) -> &Inner {
&self.inner
}
pub async fn flush(&self, timeout: Duration) -> bool {
self.inner.sink.flush(timeout).await
}
pub async fn shutdown(&self, timeout: Duration) -> bool {
self.inner.sink.shutdown(timeout).await
}
pub fn flush_blocking(&self, timeout: Duration) -> bool {
self.inner.sink.flush_blocking(timeout)
}
pub fn shutdown_blocking(&self, timeout: Duration) -> bool {
self.inner.sink.shutdown_blocking(timeout)
}
}
pub fn track<H: ServerHandler>(
handler: H,
project_id: impl Into<String>,
options: Options,
) -> Tracked<H> {
AgentCat::new(project_id, options).track(handler)
}