use std::any::{Any, TypeId};
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context as TaskContext, Poll};
use futures::{Stream, StreamExt, TryStreamExt};
use objectiveai_sdk::cli::command::{
AgentArguments, CommandExecutor, CommandRequest, CommandResponse, ResponseItem, Transform,
parse_request,
};
use serde_json::Value;
use crate::context::Context;
use crate::error::Error;
pub struct CliCommandExecutor {
ctx: Context,
}
impl CliCommandExecutor {
pub fn new(ctx: Context) -> Self {
Self { ctx }
}
}
fn extract_leaf<T: serde::de::DeserializeOwned>(value: Value) -> Result<T, serde_json::Error> {
let mut current = value;
loop {
match serde_json::from_value::<T>(current.clone()) {
Ok(t) => return Ok(t),
Err(_) => match current {
Value::Object(map) if map.len() == 1 => {
current = map.into_iter().next().unwrap().1;
}
other => return serde_json::from_value::<T>(other),
},
}
}
}
impl CliCommandExecutor {
fn resolve_ctx<'a>(
&'a self,
agent_arguments: Option<&AgentArguments>,
) -> std::borrow::Cow<'a, Context> {
match agent_arguments {
None => std::borrow::Cow::Borrowed(&self.ctx),
Some(args) => {
let mut ctx = self.ctx.clone();
ctx.config.agent_instance_hierarchy = args
.agent_instance_hierarchy
.clone()
.unwrap_or_else(|| "UNKNOWN".to_string());
ctx.config.agent_id = args.agent_id.clone();
ctx.config.agent_full_id = args.agent_full_id.clone();
ctx.config.agent_remote = args.agent_remote.clone();
ctx.config.response_id = args.response_id.clone();
ctx.config.response_ids = args.response_ids.clone();
ctx.config.mcp_session_id = args.mcp_session_id.clone();
ctx.reset_api_client();
std::borrow::Cow::Owned(ctx)
}
}
}
}
impl CommandExecutor for CliCommandExecutor {
type Error = Error;
type Stream<T>
= Pin<Box<dyn Stream<Item = Result<T, Self::Error>> + Send>>
where
T: Send + 'static;
async fn execute<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<Self::Stream<T>, Self::Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let base = request.request_base();
let transform = base.transform();
let timeout = base.timeout_seconds;
let max_tokens = base.max_tokens;
let argv = vec![
"--request".to_string(),
serde_json::to_string(&request).map_err(Error::InlineJson)?,
];
let sdk_request = parse_request(&argv).map_err(|e| match e {
objectiveai_sdk::cli::command::ParseError::Clap(e) => Error::ClapParse(e),
objectiveai_sdk::cli::command::ParseError::FromArgs(e) => Error::FromArgs(e),
})?;
let ctx = self.resolve_ctx(agent_arguments).into_owned();
let transform_ctx =
matches!(transform, Some(Transform::Python(_))).then(|| ctx.clone());
let source = futures::stream::once(async move {
crate::command::command::execute(&ctx, sdk_request).await
})
.try_flatten();
let source: Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>> =
Box::pin(source);
let mut stream: Self::Stream<T> = Box::pin(ConvertStream::new(source));
if let Some(transform) = transform {
match transform {
Transform::Python(code) => {
let ctx = transform_ctx
.expect("cloned whenever a python transform is present")
.with_no_objectiveai(true);
stream = Box::pin(PythonTransformStream::new(stream, ctx, code));
}
Transform::Jq(filter) => {
stream = Box::pin(JqTransformStream::new(stream, filter));
}
}
}
if let Some(max_tokens) = max_tokens {
stream = Box::pin(TokenCountStream::new(stream, max_tokens));
}
if let Some(timeout_seconds) = timeout {
stream = Box::pin(TimeoutStream::new(stream, timeout_seconds));
}
Ok(stream)
}
async fn execute_one<R, T>(
&self,
request: R,
agent_arguments: Option<&AgentArguments>,
) -> Result<T, Self::Error>
where
R: CommandRequest + Send + serde::Serialize,
T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
let mut stream: Self::Stream<T> =
self.execute::<R, T>(request, agent_arguments).await?;
match stream.next().await {
Some(item) => item,
None => Err(Error::EmptyStream),
}
}
}
struct ConvertStream<S, T> {
inner: S,
identity: bool,
_marker: PhantomData<fn() -> T>,
}
impl<S, T: 'static> ConvertStream<S, T> {
fn new(inner: S) -> Self {
Self {
inner,
identity: TypeId::of::<T>() == TypeId::of::<ResponseItem>(),
_marker: PhantomData,
}
}
}
impl<S, T> Stream for ConvertStream<S, T>
where
S: Stream<Item = Result<ResponseItem, Error>> + Unpin,
T: serde::de::DeserializeOwned + 'static,
{
type Item = Result<T, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(convert_item::<T>(item, this.identity))),
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
fn convert_item<T: serde::de::DeserializeOwned + 'static>(
item: ResponseItem,
identity: bool,
) -> Result<T, Error> {
if identity {
let mut slot = Some(item);
Ok((&mut slot as &mut dyn Any)
.downcast_mut::<Option<T>>()
.and_then(Option::take)
.expect("identity flag guarantees T == ResponseItem"))
} else {
let value = serde_json::to_value(item).map_err(Error::InlineJson)?;
extract_leaf::<T>(value).map_err(Error::InlineJson)
}
}
struct PythonTransformStream<S, T> {
inner: S,
ctx: Context,
code: Arc<str>,
pending: Option<Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, Error>> + Send>>>,
_marker: PhantomData<fn() -> T>,
}
impl<S, T> PythonTransformStream<S, T> {
fn new(inner: S, ctx: Context, code: String) -> Self {
Self {
inner,
ctx,
code: Arc::from(code),
pending: None,
_marker: PhantomData,
}
}
}
impl<S, T> Stream for PythonTransformStream<S, T>
where
S: Stream<Item = Result<T, Error>> + Unpin,
T: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
{
type Item = Result<T, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if let Some(fut) = this.pending.as_mut() {
match fut.as_mut().poll(cx) {
Poll::Ready(Ok(Some(value))) => {
this.pending = None;
if !value.is_null() {
return Poll::Ready(Some(
serde_json::from_value::<T>(value).map_err(Error::InlineJson),
));
}
}
Poll::Ready(Ok(None)) => this.pending = None,
Poll::Ready(Err(e)) => {
this.pending = None;
return Poll::Ready(Some(Err(e)));
}
Poll::Pending => return Poll::Pending,
}
}
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
let ctx = this.ctx.clone();
let code = Arc::clone(&this.code);
this.pending = Some(Box::pin(async move {
transform_item::<T>(&ctx, &code, item).await
}));
}
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
async fn transform_item<I: serde::Serialize>(
ctx: &Context,
code: &str,
item: I,
) -> Result<Option<serde_json::Value>, Error> {
ctx.python().await?.exec_code(ctx, code, Some(item)).await
}
struct JqTransformStream<S, T> {
inner: S,
filter: Arc<str>,
_marker: PhantomData<fn() -> T>,
}
impl<S, T> JqTransformStream<S, T> {
fn new(inner: S, filter: String) -> Self {
Self {
inner,
filter: Arc::from(filter),
_marker: PhantomData,
}
}
}
impl<S, T> Stream for JqTransformStream<S, T>
where
S: Stream<Item = Result<T, Error>> + Unpin,
T: serde::Serialize + serde::de::DeserializeOwned,
{
type Item = Result<T, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(item))) => match jq_apply::<T>(&item, &this.filter) {
Ok(Some(t)) => return Poll::Ready(Some(Ok(t))),
Ok(None) => {}
Err(e) => return Poll::Ready(Some(Err(e))),
},
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
fn jq_apply<T: serde::Serialize + serde::de::DeserializeOwned>(
item: &T,
filter: &str,
) -> Result<Option<T>, Error> {
let mut results = crate::filesystem::run_jq(item, filter).map_err(Error::Filesystem)?;
let value = match results.len() {
0 => serde_json::Value::Null,
1 => results.remove(0),
_ => serde_json::Value::Array(results),
};
if value.is_null() {
Ok(None)
} else {
serde_json::from_value::<T>(value)
.map(Some)
.map_err(Error::InlineJson)
}
}
struct TokenCountStream<S> {
inner: S,
max_tokens: u64,
total: u64,
encoder: tiktoken_rs::CoreBPE,
finished: bool,
}
impl<S> TokenCountStream<S> {
fn new(inner: S, max_tokens: u64) -> Self {
Self {
inner,
max_tokens,
total: 0,
encoder: tiktoken_rs::o200k_base()
.expect("o200k_base BPE data is embedded and always loads"),
finished: false,
}
}
}
impl<S, I> Stream for TokenCountStream<S>
where
S: Stream<Item = Result<I, Error>> + Unpin,
I: serde::Serialize,
{
type Item = Result<I, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.finished {
return Poll::Ready(None);
}
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
let json = match serde_json::to_string(&item) {
Ok(s) => s,
Err(e) => {
this.finished = true;
return Poll::Ready(Some(Err(Error::InlineJson(e))));
}
};
this.total += this.encoder.encode_with_special_tokens(&json).len() as u64;
if this.total > this.max_tokens {
this.finished = true;
return Poll::Ready(Some(Err(Error::TokenBudgetExceeded {
limit: this.max_tokens,
actual: this.total,
})));
}
Poll::Ready(Some(Ok(item)))
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => {
this.finished = true;
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}
struct TimeoutStream<S> {
inner: S,
timeout_seconds: u64,
deadline: Option<Pin<Box<tokio::time::Sleep>>>,
finished: bool,
}
impl<S> TimeoutStream<S> {
fn new(inner: S, timeout_seconds: u64) -> Self {
Self {
inner,
timeout_seconds,
deadline: None,
finished: false,
}
}
}
impl<S, I> Stream for TimeoutStream<S>
where
S: Stream<Item = Result<I, Error>> + Unpin,
{
type Item = Result<I, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.finished {
return Poll::Ready(None);
}
let secs = this.timeout_seconds;
let deadline = this.deadline.get_or_insert_with(|| {
Box::pin(tokio::time::sleep(std::time::Duration::from_secs(secs)))
});
if deadline.as_mut().poll(cx).is_ready() {
this.finished = true;
return Poll::Ready(Some(Err(Error::Timeout {
timeout_seconds: secs,
})));
}
match Pin::new(&mut this.inner).poll_next(cx) {
Poll::Ready(Some(item)) => Poll::Ready(Some(item)),
Poll::Ready(None) => {
this.finished = true;
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}