1use std::any::{Any, TypeId};
11use std::future::Future;
12use std::marker::PhantomData;
13use std::pin::Pin;
14use std::sync::Arc;
15use std::task::{Context as TaskContext, Poll};
16
17use futures::{Stream, StreamExt, TryStreamExt};
18use objectiveai_sdk::cli::command::{
19 AgentArguments, CommandExecutor, CommandRequest, CommandResponse, ResponseItem, Transform,
20 parse_request,
21};
22use serde_json::Value;
23
24use crate::context::Context;
25use crate::error::Error;
26
27pub struct CliCommandExecutor {
30 ctx: Context,
31 tee: Option<tokio::sync::mpsc::UnboundedSender<Value>>,
38}
39
40impl CliCommandExecutor {
41 pub fn new(
42 ctx: Context,
43 tee: Option<tokio::sync::mpsc::UnboundedSender<Value>>,
44 ) -> Self {
45 Self { ctx, tee }
46 }
47}
48
49fn extract_leaf<T: serde::de::DeserializeOwned>(value: Value) -> Result<T, serde_json::Error> {
55 let mut current = value;
56 loop {
57 match serde_json::from_value::<T>(current.clone()) {
58 Ok(t) => return Ok(t),
59 Err(_) => match current {
60 Value::Object(map) if map.len() == 1 => {
61 current = map.into_iter().next().unwrap().1;
62 }
63 other => return serde_json::from_value::<T>(other),
64 },
65 }
66 }
67}
68
69pub(crate) fn apply_agent_arguments<'a>(
84 base: &'a Context,
85 agent_arguments: Option<&AgentArguments>,
86) -> std::borrow::Cow<'a, Context> {
87 match agent_arguments {
88 None => std::borrow::Cow::Borrowed(base),
89 Some(args) => {
90 let mut ctx = base.clone();
91 ctx.config.agent_instance_hierarchy = args
92 .agent_instance_hierarchy
93 .clone()
94 .unwrap_or_else(|| "UNKNOWN".to_string());
95 ctx.config.agent_id = args.agent_id.clone();
96 ctx.config.agent_full_id = args.agent_full_id.clone();
97 ctx.config.agent_remote = args.agent_remote.clone();
98 ctx.config.response_id = args.response_id.clone();
99 ctx.config.response_ids = args.response_ids.clone();
100 ctx.config.mcp_session_id = args.mcp_session_id.clone();
101 ctx.reset_api_client();
102 std::borrow::Cow::Owned(ctx)
103 }
104 }
105}
106
107impl CliCommandExecutor {
108 fn resolve_ctx<'a>(
111 &'a self,
112 agent_arguments: Option<&AgentArguments>,
113 ) -> std::borrow::Cow<'a, Context> {
114 apply_agent_arguments(&self.ctx, agent_arguments)
115 }
116}
117
118impl CommandExecutor for CliCommandExecutor {
119 type Error = Error;
120 type Stream<T>
121 = Pin<Box<dyn Stream<Item = Result<T, Self::Error>> + Send>>
122 where
123 T: Send + 'static;
124
125 async fn execute<R, T>(
126 &self,
127 request: R,
128 agent_arguments: Option<&AgentArguments>,
129 ) -> Result<Self::Stream<T>, Self::Error>
130 where
131 R: CommandRequest + Send + serde::Serialize,
132 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
133 {
134 let base = request.request_base();
139 let transform = base.transform();
140 let timeout = base.timeout_seconds;
141 let max_tokens = base.max_tokens;
142
143 let argv = vec![
147 "--request".to_string(),
148 serde_json::to_string(&request).map_err(Error::InlineJson)?,
149 ];
150 let sdk_request = parse_request(&argv).map_err(|e| match e {
151 objectiveai_sdk::cli::command::ParseError::Clap(e) => Error::ClapParse(e),
152 objectiveai_sdk::cli::command::ParseError::FromArgs(e) => Error::FromArgs(e),
153 })?;
154 let ctx = self.resolve_ctx(agent_arguments).into_owned();
157 let transform_ctx =
162 matches!(transform, Some(Transform::Python(_))).then(|| ctx.clone());
163
164 let source = futures::stream::once(async move {
172 crate::command::command::execute(&ctx, sdk_request).await
173 })
174 .try_flatten();
175 let source: Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>> =
176 Box::pin(source);
177
178 let mut stream: Self::Stream<T> = Box::pin(ConvertStream::new(source));
184
185 if let Some(tee) = self.tee.clone() {
192 stream = Box::pin(TeeStream::new(stream, tee));
193 }
194
195 if let Some(transform) = transform {
201 match transform {
202 Transform::Python(code) => {
203 let ctx = transform_ctx
206 .expect("cloned whenever a python transform is present")
207 .with_no_objectiveai(true);
208 stream = Box::pin(PythonTransformStream::new(stream, ctx, code));
209 }
210 Transform::Jq(filter) => {
211 stream = Box::pin(JqTransformStream::new(stream, filter));
212 }
213 }
214 }
215
216 if let Some(max_tokens) = max_tokens {
220 stream = Box::pin(TokenCountStream::new(stream, max_tokens));
221 }
222
223 if let Some(timeout_seconds) = timeout {
227 stream = Box::pin(TimeoutStream::new(stream, timeout_seconds));
228 }
229
230 Ok(stream)
231 }
232
233 async fn execute_one<R, T>(
234 &self,
235 request: R,
236 agent_arguments: Option<&AgentArguments>,
237 ) -> Result<T, Self::Error>
238 where
239 R: CommandRequest + Send + serde::Serialize,
240 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
241 {
242 let mut stream: Self::Stream<T> =
243 self.execute::<R, T>(request, agent_arguments).await?;
244 match stream.next().await {
245 Some(item) => item,
246 None => Err(Error::EmptyStream),
247 }
248 }
249}
250
251struct TeeStream<T> {
259 inner: Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>,
260 tee: Option<tokio::sync::mpsc::UnboundedSender<Value>>,
261}
262
263impl<T> TeeStream<T> {
264 fn new(
265 inner: Pin<Box<dyn Stream<Item = Result<T, Error>> + Send>>,
266 tee: tokio::sync::mpsc::UnboundedSender<Value>,
267 ) -> Self {
268 Self {
269 inner,
270 tee: Some(tee),
271 }
272 }
273}
274
275impl<T> Stream for TeeStream<T>
276where
277 T: serde::Serialize,
278{
279 type Item = Result<T, Error>;
280
281 fn poll_next(
282 self: Pin<&mut Self>,
283 cx: &mut std::task::Context<'_>,
284 ) -> std::task::Poll<Option<Self::Item>> {
285 let this = self.get_mut();
286 let poll = this.inner.as_mut().poll_next(cx);
287 if let std::task::Poll::Ready(Some(item)) = &poll {
288 if let Some(tee) = &this.tee {
289 let json = match item {
290 Ok(value) => serde_json::to_value(value).ok(),
291 Err(e) => serde_json::to_value(objectiveai_sdk::cli::Error {
292 r#type: objectiveai_sdk::cli::ErrorType::Error,
293 level: Some(objectiveai_sdk::cli::Level::Error),
294 fatal: None,
295 message: e.output_message(),
296 })
297 .ok(),
298 };
299 if let Some(json) = json {
300 if tee.send(json).is_err() {
301 this.tee = None;
302 }
303 }
304 }
305 }
306 poll
307 }
308}
309
310struct ConvertStream<S, T> {
319 inner: S,
320 identity: bool,
322 _marker: PhantomData<fn() -> T>,
323}
324
325impl<S, T: 'static> ConvertStream<S, T> {
326 fn new(inner: S) -> Self {
327 Self {
328 inner,
329 identity: TypeId::of::<T>() == TypeId::of::<ResponseItem>(),
330 _marker: PhantomData,
331 }
332 }
333}
334
335impl<S, T> Stream for ConvertStream<S, T>
336where
337 S: Stream<Item = Result<ResponseItem, Error>> + Unpin,
338 T: serde::de::DeserializeOwned + 'static,
339{
340 type Item = Result<T, Error>;
341
342 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
343 let this = self.get_mut();
344 match Pin::new(&mut this.inner).poll_next(cx) {
345 Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(convert_item::<T>(item, this.identity))),
346 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
347 Poll::Ready(None) => Poll::Ready(None),
348 Poll::Pending => Poll::Pending,
349 }
350 }
351}
352
353fn convert_item<T: serde::de::DeserializeOwned + 'static>(
355 item: ResponseItem,
356 identity: bool,
357) -> Result<T, Error> {
358 if identity {
359 let mut slot = Some(item);
360 Ok((&mut slot as &mut dyn Any)
361 .downcast_mut::<Option<T>>()
362 .and_then(Option::take)
363 .expect("identity flag guarantees T == ResponseItem"))
364 } else {
365 let value = serde_json::to_value(item).map_err(Error::InlineJson)?;
366 extract_leaf::<T>(value).map_err(Error::InlineJson)
367 }
368}
369
370struct PythonTransformStream<S, T> {
379 inner: S,
380 ctx: Context,
381 code: Arc<str>,
382 pending: Option<Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, Error>> + Send>>>,
383 _marker: PhantomData<fn() -> T>,
384}
385
386impl<S, T> PythonTransformStream<S, T> {
387 fn new(inner: S, ctx: Context, code: String) -> Self {
388 Self {
389 inner,
390 ctx,
391 code: Arc::from(code),
392 pending: None,
393 _marker: PhantomData,
394 }
395 }
396}
397
398impl<S, T> Stream for PythonTransformStream<S, T>
399where
400 S: Stream<Item = Result<T, Error>> + Unpin,
401 T: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
402{
403 type Item = Result<T, Error>;
404
405 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
406 let this = self.get_mut();
407 loop {
408 if let Some(fut) = this.pending.as_mut() {
413 match fut.as_mut().poll(cx) {
414 Poll::Ready(Ok(Some(value))) => {
415 this.pending = None;
416 if !value.is_null() {
417 return Poll::Ready(Some(
418 serde_json::from_value::<T>(value).map_err(Error::InlineJson),
419 ));
420 }
421 }
423 Poll::Ready(Ok(None)) => this.pending = None,
425 Poll::Ready(Err(e)) => {
426 this.pending = None;
427 return Poll::Ready(Some(Err(e)));
428 }
429 Poll::Pending => return Poll::Pending,
430 }
431 }
432 match Pin::new(&mut this.inner).poll_next(cx) {
434 Poll::Ready(Some(Ok(item))) => {
435 let ctx = this.ctx.clone();
436 let code = Arc::clone(&this.code);
437 this.pending = Some(Box::pin(async move {
438 transform_item::<T>(&ctx, &code, item).await
439 }));
440 }
442 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
443 Poll::Ready(None) => return Poll::Ready(None),
444 Poll::Pending => return Poll::Pending,
445 }
446 }
447 }
448}
449
450async fn transform_item<I: serde::Serialize>(
457 ctx: &Context,
458 code: &str,
459 item: I,
460) -> Result<Option<serde_json::Value>, Error> {
461 ctx.python().await?.exec_code(ctx, code, Some(item)).await
462}
463
464struct JqTransformStream<S, T> {
475 inner: S,
476 filter: Arc<str>,
477 _marker: PhantomData<fn() -> T>,
478}
479
480impl<S, T> JqTransformStream<S, T> {
481 fn new(inner: S, filter: String) -> Self {
482 Self {
483 inner,
484 filter: Arc::from(filter),
485 _marker: PhantomData,
486 }
487 }
488}
489
490impl<S, T> Stream for JqTransformStream<S, T>
491where
492 S: Stream<Item = Result<T, Error>> + Unpin,
493 T: serde::Serialize + serde::de::DeserializeOwned,
494{
495 type Item = Result<T, Error>;
496
497 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
498 let this = self.get_mut();
499 loop {
500 match Pin::new(&mut this.inner).poll_next(cx) {
501 Poll::Ready(Some(Ok(item))) => match jq_apply::<T>(&item, &this.filter) {
502 Ok(Some(t)) => return Poll::Ready(Some(Ok(t))),
503 Ok(None) => {}
505 Err(e) => return Poll::Ready(Some(Err(e))),
506 },
507 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
508 Poll::Ready(None) => return Poll::Ready(None),
509 Poll::Pending => return Poll::Pending,
510 }
511 }
512 }
513}
514
515fn jq_apply<T: serde::Serialize + serde::de::DeserializeOwned>(
519 item: &T,
520 filter: &str,
521) -> Result<Option<T>, Error> {
522 let mut results = crate::filesystem::run_jq(item, filter).map_err(Error::Filesystem)?;
523 let value = match results.len() {
524 0 => serde_json::Value::Null,
525 1 => results.remove(0),
526 _ => serde_json::Value::Array(results),
527 };
528 if value.is_null() {
529 Ok(None)
530 } else {
531 serde_json::from_value::<T>(value)
532 .map(Some)
533 .map_err(Error::InlineJson)
534 }
535}
536
537struct TokenCountStream<S> {
546 inner: S,
547 max_tokens: u64,
548 total: u64,
549 encoder: tiktoken_rs::CoreBPE,
550 finished: bool,
552}
553
554impl<S> TokenCountStream<S> {
555 fn new(inner: S, max_tokens: u64) -> Self {
556 Self {
557 inner,
558 max_tokens,
559 total: 0,
560 encoder: tiktoken_rs::o200k_base()
562 .expect("o200k_base BPE data is embedded and always loads"),
563 finished: false,
564 }
565 }
566}
567
568impl<S, I> Stream for TokenCountStream<S>
569where
570 S: Stream<Item = Result<I, Error>> + Unpin,
571 I: serde::Serialize,
572{
573 type Item = Result<I, Error>;
574
575 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
576 let this = self.get_mut();
577 if this.finished {
578 return Poll::Ready(None);
579 }
580 match Pin::new(&mut this.inner).poll_next(cx) {
581 Poll::Ready(Some(Ok(item))) => {
582 let json = match serde_json::to_string(&item) {
584 Ok(s) => s,
585 Err(e) => {
586 this.finished = true;
587 return Poll::Ready(Some(Err(Error::InlineJson(e))));
588 }
589 };
590 this.total += this.encoder.encode_with_special_tokens(&json).len() as u64;
591 if this.total > this.max_tokens {
592 this.finished = true;
593 return Poll::Ready(Some(Err(Error::TokenBudgetExceeded {
594 limit: this.max_tokens,
595 actual: this.total,
596 })));
597 }
598 Poll::Ready(Some(Ok(item)))
599 }
600 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
601 Poll::Ready(None) => {
602 this.finished = true;
603 Poll::Ready(None)
604 }
605 Poll::Pending => Poll::Pending,
606 }
607 }
608}
609
610struct TimeoutStream<S> {
619 inner: S,
620 timeout_seconds: u64,
621 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
623 finished: bool,
625}
626
627impl<S> TimeoutStream<S> {
628 fn new(inner: S, timeout_seconds: u64) -> Self {
629 Self {
630 inner,
631 timeout_seconds,
632 deadline: None,
633 finished: false,
634 }
635 }
636}
637
638impl<S, I> Stream for TimeoutStream<S>
639where
640 S: Stream<Item = Result<I, Error>> + Unpin,
641{
642 type Item = Result<I, Error>;
643
644 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
645 let this = self.get_mut();
646 if this.finished {
647 return Poll::Ready(None);
648 }
649 let secs = this.timeout_seconds;
650 let deadline = this.deadline.get_or_insert_with(|| {
651 Box::pin(tokio::time::sleep(std::time::Duration::from_secs(secs)))
652 });
653 if deadline.as_mut().poll(cx).is_ready() {
655 this.finished = true;
656 return Poll::Ready(Some(Err(Error::Timeout {
657 timeout_seconds: secs,
658 })));
659 }
660 match Pin::new(&mut this.inner).poll_next(cx) {
661 Poll::Ready(Some(item)) => Poll::Ready(Some(item)),
662 Poll::Ready(None) => {
663 this.finished = true;
664 Poll::Ready(None)
665 }
666 Poll::Pending => Poll::Pending,
667 }
668 }
669}