use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::marker::PhantomData;
use std::sync::Arc;
use std::pin::Pin;
use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
use objectiveai_sdk::agent::completions::response::streaming::{
AgentCompletionChunk, AgentCompletionIds,
};
use objectiveai_sdk::functions::executions::request::FunctionExecutionCreateParams;
use objectiveai_sdk::functions::executions::response::streaming::FunctionExecutionChunk;
use objectiveai_sdk::vector::completions::request::VectorCompletionCreateParams;
use objectiveai_sdk::vector::completions::response::streaming::VectorCompletionChunk;
use serde::Serialize;
use tokio::sync::{Mutex, mpsc, oneshot, watch};
use tokio::task::JoinHandle;
use crate::db::Pool;
use super::row::{RowValue, WriterItem, WriterItems};
use super::rows::{
agent_completion_chunk_rows, function_execution_chunk_rows,
vector_completion_chunk_rows,
};
use super::shadow::{Shadow, WriteOp};
use super::write::{
Tier, insert_request_blob, insert_request_messages_row, insert_response_blob,
update_agent_token_usage, write_value,
};
pub trait WriterChunk {
fn primary_id(&self) -> &str;
fn agent_tier_aih(&self) -> Option<&str> {
None
}
}
impl WriterChunk for AgentCompletionChunk {
fn primary_id(&self) -> &str {
self.id.as_str()
}
fn agent_tier_aih(&self) -> Option<&str> {
Some(self.agent_instance_hierarchy.as_str())
}
}
impl WriterChunk for VectorCompletionChunk {
fn primary_id(&self) -> &str {
self.id.as_str()
}
}
impl WriterChunk for FunctionExecutionChunk {
fn primary_id(&self) -> &str {
self.id.as_str()
}
}
pub trait ChunkAgentRefs {
fn collect_agent_refs(
&self,
out: &mut Vec<(String, crate::db::agent_refs::AgentRefValue)>,
);
}
impl ChunkAgentRefs for AgentCompletionChunk {
fn collect_agent_refs(
&self,
_out: &mut Vec<(String, crate::db::agent_refs::AgentRefValue)>,
) {
}
}
impl ChunkAgentRefs for VectorCompletionChunk {
fn collect_agent_refs(
&self,
out: &mut Vec<(String, crate::db::agent_refs::AgentRefValue)>,
) {
for completion in &self.completions {
if let Some(inline) = &completion.agent_inline {
let value = match &completion.inner.agent_remote {
Some(remote) => {
crate::db::agent_refs::AgentRefValue::remote(remote)
}
None => crate::db::agent_refs::AgentRefValue::inline(inline),
};
if let Some(value) = value {
out.push((
completion.inner.agent_instance_hierarchy.clone(),
value,
));
}
}
}
}
}
impl ChunkAgentRefs for FunctionExecutionChunk {
fn collect_agent_refs(
&self,
out: &mut Vec<(String, crate::db::agent_refs::AgentRefValue)>,
) {
use objectiveai_sdk::functions::executions::response::streaming::TaskChunk;
for task in &self.tasks {
match task {
TaskChunk::FunctionExecution(wrapper) => {
wrapper.inner.collect_agent_refs(out);
}
TaskChunk::VectorCompletion(wrapper) => {
wrapper.inner.collect_agent_refs(out);
}
}
}
if let Some(reasoning) = &self.reasoning {
reasoning.inner.collect_agent_refs(out);
}
}
}
pub trait ChunkPush {
fn push(&mut self, other: &Self);
}
impl ChunkPush for AgentCompletionChunk {
fn push(&mut self, other: &Self) {
AgentCompletionChunk::push(self, other);
}
}
impl ChunkPush for VectorCompletionChunk {
fn push(&mut self, other: &Self) {
VectorCompletionChunk::push(self, other);
}
}
impl ChunkPush for FunctionExecutionChunk {
fn push(&mut self, other: &Self) {
FunctionExecutionChunk::push(self, other);
}
}
pub struct LogWriter<C> {
tx: mpsc::UnboundedSender<C>,
handle: JoinHandle<Result<(), crate::error::Error>>,
written_rx: watch::Receiver<bool>,
stream_error: Arc<Mutex<Option<serde_json::Value>>>,
_chunk: PhantomData<fn() -> C>,
}
impl<C> LogWriter<C> {
pub fn write(&self, chunk: C) -> Result<(), crate::error::Error> {
self.tx
.send(chunk)
.map_err(|_| crate::error::Error::Instance(
"log writer task has exited (earlier write failed)".to_string(),
))
}
pub fn written_once(&self) -> bool {
*self.written_rx.borrow()
}
pub async fn wait_written_once(&self) -> Result<(), crate::error::Error> {
let mut rx = self.written_rx.clone();
rx.wait_for(|b| *b)
.await
.map(|_| ())
.map_err(|_| crate::error::Error::Instance(
"log writer task exited before completing its first write".to_string(),
))
}
pub async fn finalize(self) -> Result<(), crate::error::Error> {
let LogWriter { tx, handle, .. } = self;
drop(tx);
match handle.await {
Ok(inner) => inner,
Err(e) => Err(crate::error::Error::Instance(
format!("log writer task: {e}"),
)),
}
}
pub async fn finalize_with_stream_error(
self,
error: Option<serde_json::Value>,
) -> Result<(), crate::error::Error> {
if let Some(value) = error {
*self.stream_error.lock().await = Some(value);
}
self.finalize().await
}
}
struct LogWriterState<C> {
pool: Pool,
tier: Tier,
request_body: serde_json::Value,
request_messages: Option<Vec<objectiveai_sdk::agent::completions::message::Message>>,
request_agent_ref: Option<crate::db::agent_refs::AgentRefValue>,
sender_agent_instance_hierarchy: String,
items_fn: for<'a> fn(&'a C) -> WriterItems<'a>,
last_usage: HashMap<String, u64>,
primary_id: Option<String>,
shadow: Shadow,
seen_agents: HashSet<String>,
tee: Option<super::tee::ConversationTee>,
frame_mapper: super::tee::FrameMapper,
logged_errors: HashSet<(String, String)>,
statuses_fn: for<'a> fn(&'a C) -> super::rows::CompletionStatuses<'a>,
chunk_error_fn: for<'a> fn(&'a C) -> Option<&'a objectiveai_sdk::error::ResponseError>,
stream_error: Arc<Mutex<Option<serde_json::Value>>>,
_chunk: PhantomData<fn() -> C>,
}
impl<C> LogWriterState<C> {
fn new(
pool: Pool,
tier: Tier,
request_body: serde_json::Value,
sender_agent_instance_hierarchy: String,
items_fn: for<'a> fn(&'a C) -> WriterItems<'a>,
request_messages: Option<
Vec<objectiveai_sdk::agent::completions::message::Message>,
>,
request_agent_ref: Option<crate::db::agent_refs::AgentRefValue>,
tee: Option<super::tee::ConversationTee>,
statuses_fn: for<'a> fn(&'a C) -> super::rows::CompletionStatuses<'a>,
chunk_error_fn: for<'a> fn(&'a C) -> Option<&'a objectiveai_sdk::error::ResponseError>,
stream_error: Arc<Mutex<Option<serde_json::Value>>>,
) -> Self {
Self {
pool,
tier,
request_body,
request_messages,
request_agent_ref,
sender_agent_instance_hierarchy,
items_fn,
last_usage: HashMap::new(),
primary_id: None,
shadow: Shadow::new(),
seen_agents: HashSet::new(),
tee,
frame_mapper: super::tee::FrameMapper::default(),
logged_errors: HashSet::new(),
statuses_fn,
chunk_error_fn,
stream_error,
_chunk: PhantomData,
}
}
async fn write_agent_request_messages(
&mut self,
response_id: &str,
aih: &str,
) -> Result<(), crate::error::Error> {
let Some(messages) = &self.request_messages else {
return Ok(());
};
let ts = now_secs() as i64;
for row in super::rows::request_message_rows(response_id, aih, messages) {
if let Some(tee) = &self.tee {
if let Some(frame) = self.frame_mapper.map(&row, ts) {
tee.send(frame);
}
}
write_value(&self.pool, WriteOp::Insert, &row, ts).await?;
}
Ok(())
}
async fn apply_chunk(&mut self, chunk: &C) -> Result<(), crate::error::Error>
where
C: Send + Sync,
{
let response_id = self
.primary_id
.clone()
.expect("primary_id set by listener_loop before apply_chunk");
let created_at_seed = now_secs() as i64;
let mut buckets: HashMap<&str, Vec<(WriteOp, RowValue<'_>)>> = HashMap::new();
for item in (self.items_fn)(chunk) {
match item {
WriterItem::Row(value) => {
let key = value.agent_instance_hierarchy();
match self.shadow.record(&value) {
WriteOp::Skip => {}
op => {
if let Some(tee) = &self.tee {
if let Some(frame) =
self.frame_mapper.map(&value, created_at_seed)
{
tee.send(frame);
}
}
buckets.entry(key).or_default().push((op, value));
}
}
}
WriterItem::Usage { agent_instance_hierarchy, total_tokens } => {
if self.last_usage.get(agent_instance_hierarchy) != Some(&total_tokens) {
update_agent_token_usage(
&self.pool,
agent_instance_hierarchy,
total_tokens as i64,
)
.await?;
self.last_usage
.insert(agent_instance_hierarchy.to_string(), total_tokens);
}
}
WriterItem::Error { agent_instance_hierarchy, response_id: rid, error } => {
let logged_key =
(agent_instance_hierarchy.to_string(), rid.to_string());
if !self.logged_errors.contains(&logged_key) {
let value = serde_json::to_value(error)
.unwrap_or_else(|_| error.to_string().into());
super::errors::insert_error(
&self.pool,
agent_instance_hierarchy,
Some(rid),
&value,
created_at_seed,
)
.await?;
if let Some(tee) = &self.tee {
tee.send(super::tee::error_frame(
agent_instance_hierarchy.to_string(),
Some(rid.to_string()),
value,
created_at_seed,
));
}
self.logged_errors.insert(logged_key);
}
}
}
}
let pool = &self.pool;
let tier = self.tier;
let resp_id = response_id.as_str();
let seen_agents = &mut self.seen_agents;
let bucket_futures: Vec<
Pin<Box<dyn Future<Output = Result<(), crate::error::Error>> + Send + '_>>,
> = buckets
.into_iter()
.map(|(hier, items)| {
let needs_request_row = !seen_agents.contains(hier);
if needs_request_row {
seen_agents.insert(hier.to_string());
}
Box::pin(async move {
if needs_request_row {
insert_request_messages_row(
pool,
tier,
resp_id,
hier,
created_at_seed,
)
.await?;
}
for (op, value) in &items {
write_value(pool, *op, value, created_at_seed).await?;
}
Ok::<(), crate::error::Error>(())
})
as Pin<
Box<
dyn Future<Output = Result<(), crate::error::Error>>
+ Send
+ '_,
>,
>
})
.collect();
futures::future::try_join_all(bucket_futures).await?;
Ok(())
}
async fn write_request_blob(
&self,
response_id: &str,
) -> Result<(), crate::error::Error> {
let created_at_seed = now_secs() as i64;
insert_request_blob(
&self.pool,
self.tier,
response_id,
&self.request_body,
&self.sender_agent_instance_hierarchy,
created_at_seed,
)
.await?;
Ok(())
}
async fn write_response_blob(
&self,
chunk: &C,
) -> Result<(), crate::error::Error>
where
C: Serialize,
{
let Some(response_id) = self.primary_id.as_deref() else {
return Ok(());
};
let created_at_seed = now_secs() as i64;
insert_response_blob(
&self.pool,
self.tier,
response_id,
chunk,
created_at_seed,
)
.await?;
Ok(())
}
}
async fn listener_loop<C>(
mut rx: mpsc::UnboundedReceiver<C>,
mut state: LogWriterState<C>,
ready_tx: oneshot::Sender<String>,
written_tx: watch::Sender<bool>,
) -> Result<(), crate::error::Error>
where
C: WriterChunk
+ AgentCompletionIds
+ ChunkAgentRefs
+ ChunkPush
+ Clone
+ Serialize
+ Send
+ Sync,
{
let mut ready_tx = Some(ready_tx);
let mut written_fired = false;
let mut accumulated: Option<C> = None;
while let Some(first) = rx.recv().await {
let mut agent_refs: Vec<(String, crate::db::agent_refs::AgentRefValue)> =
Vec::new();
first.collect_agent_refs(&mut agent_refs);
if let Some(acc) = accumulated.as_mut() {
acc.push(&first);
} else {
accumulated = Some(first.clone());
}
while let Ok(next) = rx.try_recv() {
next.collect_agent_refs(&mut agent_refs);
if let Some(acc) = accumulated.as_mut() {
acc.push(&next);
}
}
let acc = accumulated
.as_ref()
.expect("accumulated is Some: set or pushed above");
if state.primary_id.is_none() {
let response_id = acc.primary_id().to_string();
state.write_request_blob(&response_id).await?;
if let Some(aih) = acc.agent_tier_aih() {
state.write_agent_request_messages(&response_id, aih).await?;
if let Some(value) = state.request_agent_ref.take() {
crate::db::agent_refs::upsert(&state.pool, aih, value)
.await?;
}
}
state.primary_id = Some(response_id);
}
state.apply_chunk(acc).await?;
for (hier, value) in agent_refs {
crate::db::agent_refs::upsert(&state.pool, &hier, value).await?;
}
if !written_fired {
let _ = written_tx.send(true);
written_fired = true;
}
if let Some(tx) = ready_tx.take() {
match state.primary_id.as_deref() {
Some(id) => {
let _ = tx.send(id.to_string());
}
None => {
ready_tx = Some(tx);
}
}
}
}
if let Some(acc) = accumulated {
let sweep_error = state.stream_error.lock().await.take().or_else(|| {
(state.chunk_error_fn)(&acc).map(|e| {
serde_json::to_value(e).unwrap_or_else(|_| e.to_string().into())
})
});
if let Some(value) = sweep_error {
let ts = now_secs() as i64;
for status in (state.statuses_fn)(&acc) {
if status.finished || status.errored {
continue;
}
let key = (
status.agent_instance_hierarchy.to_string(),
status.response_id.to_string(),
);
if state.logged_errors.contains(&key) {
continue;
}
super::errors::insert_error(
&state.pool,
status.agent_instance_hierarchy,
Some(status.response_id),
&value,
ts,
)
.await?;
if let Some(tee) = &state.tee {
tee.send(super::tee::error_frame(
key.0.clone(),
Some(key.1.clone()),
value.clone(),
ts,
));
}
state.logged_errors.insert(key);
}
}
state.write_response_blob(&acc).await?;
}
Ok(())
}
fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn spawn_writer<C>(
pool: Pool,
tier: Tier,
request_body: serde_json::Value,
sender_agent_instance_hierarchy: String,
items_fn: for<'a> fn(&'a C) -> WriterItems<'a>,
request_messages: Option<
Vec<objectiveai_sdk::agent::completions::message::Message>,
>,
request_agent_ref: Option<crate::db::agent_refs::AgentRefValue>,
tee: Option<super::tee::ConversationTee>,
statuses_fn: for<'a> fn(&'a C) -> super::rows::CompletionStatuses<'a>,
chunk_error_fn: for<'a> fn(&'a C) -> Option<&'a objectiveai_sdk::error::ResponseError>,
) -> (LogWriter<C>, oneshot::Receiver<String>)
where
C: WriterChunk
+ AgentCompletionIds
+ ChunkAgentRefs
+ ChunkPush
+ Clone
+ Serialize
+ Send
+ Sync
+ 'static,
{
let (tx, rx) = mpsc::unbounded_channel();
let (ready_tx, ready_rx) = oneshot::channel();
let (written_tx, written_rx) = watch::channel(false);
let stream_error: Arc<Mutex<Option<serde_json::Value>>> =
Arc::new(Mutex::new(None));
let state = LogWriterState::new(
pool,
tier,
request_body,
sender_agent_instance_hierarchy,
items_fn,
request_messages,
request_agent_ref,
tee,
statuses_fn,
chunk_error_fn,
stream_error.clone(),
);
let handle = tokio::spawn(listener_loop(rx, state, ready_tx, written_tx));
(
LogWriter {
tx,
handle,
written_rx,
stream_error,
_chunk: PhantomData,
},
ready_rx,
)
}
pub fn write_agent_completion(
pool: &Pool,
params: &AgentCompletionCreateParams,
sender_agent_instance_hierarchy: String,
tee: Option<super::tee::ConversationTee>,
) -> Result<
(LogWriter<AgentCompletionChunk>, oneshot::Receiver<String>),
crate::error::Error,
> {
let body = serde_json::to_value(params)?;
let request_messages = Some(params.messages.clone());
let request_agent_ref = match ¶ms.agent {
objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional::AgentBase(base) => {
crate::db::agent_refs::AgentRefValue::inline(base)
}
objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional::Remote(remote) => {
crate::db::agent_refs::AgentRefValue::remote(remote)
}
};
Ok(spawn_writer(
pool.clone(),
Tier::Agent,
body,
sender_agent_instance_hierarchy,
agent_completion_chunk_rows,
request_messages,
request_agent_ref,
tee,
super::rows::agent_completion_statuses,
|_| None,
))
}
pub fn write_vector_completion(
pool: &Pool,
params: &VectorCompletionCreateParams,
sender_agent_instance_hierarchy: String,
tee: Option<super::tee::ConversationTee>,
) -> Result<
(LogWriter<VectorCompletionChunk>, oneshot::Receiver<String>),
crate::error::Error,
> {
let body = serde_json::to_value(params)?;
Ok(spawn_writer(
pool.clone(),
Tier::Vector,
body,
sender_agent_instance_hierarchy,
vector_completion_chunk_rows,
None,
None,
tee,
super::rows::vector_completion_statuses,
|_| None,
))
}
pub fn write_function_execution(
pool: &Pool,
params: &FunctionExecutionCreateParams,
sender_agent_instance_hierarchy: String,
tee: Option<super::tee::ConversationTee>,
) -> Result<
(LogWriter<FunctionExecutionChunk>, oneshot::Receiver<String>),
crate::error::Error,
> {
let body = serde_json::to_value(params)?;
Ok(spawn_writer(
pool.clone(),
Tier::Function,
body,
sender_agent_instance_hierarchy,
function_execution_chunk_rows,
None,
None,
tee,
super::rows::function_execution_statuses,
|chunk| chunk.error.as_ref(),
))
}