use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::harness::events::{AgentEvent, EventListener, HarnessRunStatus};
use crate::harness::ids::{CallId, EventId, RunId};
use crate::harness::store::{AppendStore, JsonlAppendStore};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AgentObservation {
pub event_id: EventId,
pub run_id: RunId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_run_id: Option<RunId>,
pub root_run_id: RunId,
pub offset: u64,
pub ts_ms: u64,
pub event: AgentEvent,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentCallLatency {
pub call_id: CallId,
pub kind: String,
pub name: String,
pub elapsed_ms: u64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentLatencyMetrics {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_elapsed_ms: Option<u64>,
#[serde(default)]
pub model_calls: Vec<AgentCallLatency>,
#[serde(default)]
pub tool_calls: Vec<AgentCallLatency>,
pub total_model_ms: u64,
pub max_model_ms: u64,
pub total_tool_ms: u64,
pub max_tool_ms: u64,
}
impl AgentObservation {
pub fn from_record(
record: &crate::harness::events::EventRecord,
run_id: RunId,
parent_run_id: Option<RunId>,
root_run_id: RunId,
) -> Self {
Self {
event_id: record.id.clone(),
run_id,
parent_run_id,
root_run_id,
offset: record.offset,
ts_ms: now_ms(),
event: record.event.clone(),
}
}
}
#[async_trait]
pub trait HarnessEventJournal: Send + Sync {
async fn append(&self, obs: AgentObservation) -> Result<u64>;
async fn read_from(&self, run_id: &str, offset: u64) -> Result<Vec<AgentObservation>>;
async fn read_window(
&self,
run_id: &str,
offset: u64,
limit: usize,
) -> Result<Vec<AgentObservation>> {
let mut all = self.read_from(run_id, offset).await?;
all.truncate(limit);
Ok(all)
}
async fn read_filtered(
&self,
run_id: &str,
offset: u64,
kinds: &[&str],
) -> Result<Vec<AgentObservation>> {
let all = self.read_from(run_id, offset).await?;
Ok(all
.into_iter()
.filter(|obs| kinds.is_empty() || kinds.contains(&obs.event.kind()))
.collect())
}
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryEventJournal {
pub(crate) runs: Arc<Mutex<HashMap<String, Vec<AgentObservation>>>>,
}
#[derive(Clone, Debug)]
pub struct StoreEventJournal<A: AppendStore> {
pub(crate) store: A,
}
#[async_trait]
pub trait HarnessStatusStore: Send + Sync {
async fn put_status(&self, status: HarnessRunStatus) -> Result<()>;
async fn get_status(&self, run_id: &str) -> Result<Option<HarnessRunStatus>>;
async fn list_by_thread(&self, thread_id: &str) -> Result<Vec<HarnessRunStatus>>;
async fn list_by_root(&self, root_run_id: &str) -> Result<Vec<HarnessRunStatus>> {
let _ = root_run_id;
Ok(Vec::new())
}
async fn list_active(&self) -> Result<Vec<HarnessRunStatus>> {
Ok(Vec::new())
}
}
#[derive(Clone, Debug, Default)]
pub struct InMemoryStatusStore {
pub(crate) statuses: Arc<Mutex<HashMap<String, HarnessRunStatus>>>,
}
#[derive(Clone, Default)]
pub struct FanOutSink {
pub(crate) listeners: Vec<Arc<dyn EventListener>>,
}
#[derive(Clone)]
pub struct RedactingSink {
pub(crate) inner: Arc<dyn EventListener>,
pub(crate) secrets: Vec<String>,
pub(crate) mask: String,
}
#[derive(Clone)]
pub struct JournalSink {
pub(crate) journal: Arc<dyn HarnessEventJournal>,
pub(crate) run_id: RunId,
pub(crate) parent_run_id: Option<RunId>,
pub(crate) root_run_id: RunId,
}
#[derive(Clone, Debug)]
pub struct JsonlSink {
pub(crate) store: JsonlAppendStore,
pub(crate) stream: String,
}
pub(crate) fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}