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}
32
33impl CliCommandExecutor {
34 pub fn new(ctx: Context) -> Self {
35 Self { ctx }
36 }
37}
38
39fn extract_leaf<T: serde::de::DeserializeOwned>(value: Value) -> Result<T, serde_json::Error> {
45 let mut current = value;
46 loop {
47 match serde_json::from_value::<T>(current.clone()) {
48 Ok(t) => return Ok(t),
49 Err(_) => match current {
50 Value::Object(map) if map.len() == 1 => {
51 current = map.into_iter().next().unwrap().1;
52 }
53 other => return serde_json::from_value::<T>(other),
54 },
55 }
56 }
57}
58
59impl CliCommandExecutor {
60 fn resolve_ctx<'a>(
73 &'a self,
74 agent_arguments: Option<&AgentArguments>,
75 ) -> std::borrow::Cow<'a, Context> {
76 match agent_arguments {
77 None => std::borrow::Cow::Borrowed(&self.ctx),
78 Some(args) => {
79 let mut ctx = self.ctx.clone();
80 ctx.config.agent_instance_hierarchy = args
81 .agent_instance_hierarchy
82 .clone()
83 .unwrap_or_else(|| "UNKNOWN".to_string());
84 ctx.config.agent_id = args.agent_id.clone();
85 ctx.config.agent_full_id = args.agent_full_id.clone();
86 ctx.config.agent_remote = args.agent_remote.clone();
87 ctx.config.response_id = args.response_id.clone();
88 ctx.config.response_ids = args.response_ids.clone();
89 ctx.config.mcp_session_id = args.mcp_session_id.clone();
90 ctx.reset_api_client();
91 std::borrow::Cow::Owned(ctx)
92 }
93 }
94 }
95}
96
97impl CommandExecutor for CliCommandExecutor {
98 type Error = Error;
99 type Stream<T>
100 = Pin<Box<dyn Stream<Item = Result<T, Self::Error>> + Send>>
101 where
102 T: Send + 'static;
103
104 async fn execute<R, T>(
105 &self,
106 request: R,
107 agent_arguments: Option<&AgentArguments>,
108 ) -> Result<Self::Stream<T>, Self::Error>
109 where
110 R: CommandRequest + Send + serde::Serialize,
111 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
112 {
113 let base = request.request_base();
118 let transform = base.transform();
119 let timeout = base.timeout_seconds;
120 let max_tokens = base.max_tokens;
121
122 let argv = vec![
126 "--request".to_string(),
127 serde_json::to_string(&request).map_err(Error::InlineJson)?,
128 ];
129 let sdk_request = parse_request(&argv).map_err(|e| match e {
130 objectiveai_sdk::cli::command::ParseError::Clap(e) => Error::ClapParse(e),
131 objectiveai_sdk::cli::command::ParseError::FromArgs(e) => Error::FromArgs(e),
132 })?;
133 let ctx = self.resolve_ctx(agent_arguments).into_owned();
136 let transform_ctx =
141 matches!(transform, Some(Transform::Python(_))).then(|| ctx.clone());
142
143 let source = futures::stream::once(async move {
151 crate::command::command::execute(&ctx, sdk_request).await
152 })
153 .try_flatten();
154 let source: Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>> =
155 Box::pin(source);
156
157 let mut stream: Self::Stream<T> = Box::pin(ConvertStream::new(source));
163
164 if let Some(transform) = transform {
170 match transform {
171 Transform::Python(code) => {
172 let ctx = transform_ctx.expect("cloned whenever a python transform is present");
173 stream = Box::pin(PythonTransformStream::new(stream, ctx, code));
174 }
175 Transform::Jq(filter) => {
176 stream = Box::pin(JqTransformStream::new(stream, filter));
177 }
178 }
179 }
180
181 if let Some(max_tokens) = max_tokens {
185 stream = Box::pin(TokenCountStream::new(stream, max_tokens));
186 }
187
188 if let Some(timeout_seconds) = timeout {
192 stream = Box::pin(TimeoutStream::new(stream, timeout_seconds));
193 }
194
195 Ok(stream)
196 }
197
198 async fn execute_one<R, T>(
199 &self,
200 request: R,
201 agent_arguments: Option<&AgentArguments>,
202 ) -> Result<T, Self::Error>
203 where
204 R: CommandRequest + Send + serde::Serialize,
205 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
206 {
207 let mut stream: Self::Stream<T> =
208 self.execute::<R, T>(request, agent_arguments).await?;
209 match stream.next().await {
210 Some(item) => item,
211 None => Err(Error::EmptyStream),
212 }
213 }
214}
215
216struct ConvertStream<S, T> {
225 inner: S,
226 identity: bool,
228 _marker: PhantomData<fn() -> T>,
229}
230
231impl<S, T: 'static> ConvertStream<S, T> {
232 fn new(inner: S) -> Self {
233 Self {
234 inner,
235 identity: TypeId::of::<T>() == TypeId::of::<ResponseItem>(),
236 _marker: PhantomData,
237 }
238 }
239}
240
241impl<S, T> Stream for ConvertStream<S, T>
242where
243 S: Stream<Item = Result<ResponseItem, Error>> + Unpin,
244 T: serde::de::DeserializeOwned + 'static,
245{
246 type Item = Result<T, Error>;
247
248 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
249 let this = self.get_mut();
250 match Pin::new(&mut this.inner).poll_next(cx) {
251 Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(convert_item::<T>(item, this.identity))),
252 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
253 Poll::Ready(None) => Poll::Ready(None),
254 Poll::Pending => Poll::Pending,
255 }
256 }
257}
258
259fn convert_item<T: serde::de::DeserializeOwned + 'static>(
261 item: ResponseItem,
262 identity: bool,
263) -> Result<T, Error> {
264 if identity {
265 let mut slot = Some(item);
266 Ok((&mut slot as &mut dyn Any)
267 .downcast_mut::<Option<T>>()
268 .and_then(Option::take)
269 .expect("identity flag guarantees T == ResponseItem"))
270 } else {
271 let value = serde_json::to_value(item).map_err(Error::InlineJson)?;
272 extract_leaf::<T>(value).map_err(Error::InlineJson)
273 }
274}
275
276struct PythonTransformStream<S, T> {
285 inner: S,
286 ctx: Context,
287 code: Arc<str>,
288 pending: Option<Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, Error>> + Send>>>,
289 _marker: PhantomData<fn() -> T>,
290}
291
292impl<S, T> PythonTransformStream<S, T> {
293 fn new(inner: S, ctx: Context, code: String) -> Self {
294 Self {
295 inner,
296 ctx,
297 code: Arc::from(code),
298 pending: None,
299 _marker: PhantomData,
300 }
301 }
302}
303
304impl<S, T> Stream for PythonTransformStream<S, T>
305where
306 S: Stream<Item = Result<T, Error>> + Unpin,
307 T: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
308{
309 type Item = Result<T, Error>;
310
311 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
312 let this = self.get_mut();
313 loop {
314 if let Some(fut) = this.pending.as_mut() {
319 match fut.as_mut().poll(cx) {
320 Poll::Ready(Ok(Some(value))) => {
321 this.pending = None;
322 if !value.is_null() {
323 return Poll::Ready(Some(
324 serde_json::from_value::<T>(value).map_err(Error::InlineJson),
325 ));
326 }
327 }
329 Poll::Ready(Ok(None)) => this.pending = None,
331 Poll::Ready(Err(e)) => {
332 this.pending = None;
333 return Poll::Ready(Some(Err(e)));
334 }
335 Poll::Pending => return Poll::Pending,
336 }
337 }
338 match Pin::new(&mut this.inner).poll_next(cx) {
340 Poll::Ready(Some(Ok(item))) => {
341 let ctx = this.ctx.clone();
342 let code = Arc::clone(&this.code);
343 this.pending = Some(Box::pin(async move {
344 transform_item::<T>(&ctx, &code, item).await
345 }));
346 }
348 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
349 Poll::Ready(None) => return Poll::Ready(None),
350 Poll::Pending => return Poll::Pending,
351 }
352 }
353 }
354}
355
356async fn transform_item<I: serde::Serialize>(
363 ctx: &Context,
364 code: &str,
365 item: I,
366) -> Result<Option<serde_json::Value>, Error> {
367 ctx.python().await?.exec_code(ctx, code, Some(item)).await
368}
369
370struct JqTransformStream<S, T> {
381 inner: S,
382 filter: Arc<str>,
383 _marker: PhantomData<fn() -> T>,
384}
385
386impl<S, T> JqTransformStream<S, T> {
387 fn new(inner: S, filter: String) -> Self {
388 Self {
389 inner,
390 filter: Arc::from(filter),
391 _marker: PhantomData,
392 }
393 }
394}
395
396impl<S, T> Stream for JqTransformStream<S, T>
397where
398 S: Stream<Item = Result<T, Error>> + Unpin,
399 T: serde::Serialize + serde::de::DeserializeOwned,
400{
401 type Item = Result<T, Error>;
402
403 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
404 let this = self.get_mut();
405 loop {
406 match Pin::new(&mut this.inner).poll_next(cx) {
407 Poll::Ready(Some(Ok(item))) => match jq_apply::<T>(&item, &this.filter) {
408 Ok(Some(t)) => return Poll::Ready(Some(Ok(t))),
409 Ok(None) => {}
411 Err(e) => return Poll::Ready(Some(Err(e))),
412 },
413 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
414 Poll::Ready(None) => return Poll::Ready(None),
415 Poll::Pending => return Poll::Pending,
416 }
417 }
418 }
419}
420
421fn jq_apply<T: serde::Serialize + serde::de::DeserializeOwned>(
425 item: &T,
426 filter: &str,
427) -> Result<Option<T>, Error> {
428 let mut results = crate::filesystem::run_jq(item, filter).map_err(Error::Filesystem)?;
429 let value = match results.len() {
430 0 => serde_json::Value::Null,
431 1 => results.remove(0),
432 _ => serde_json::Value::Array(results),
433 };
434 if value.is_null() {
435 Ok(None)
436 } else {
437 serde_json::from_value::<T>(value)
438 .map(Some)
439 .map_err(Error::InlineJson)
440 }
441}
442
443struct TokenCountStream<S> {
452 inner: S,
453 max_tokens: u64,
454 total: u64,
455 encoder: tiktoken_rs::CoreBPE,
456 finished: bool,
458}
459
460impl<S> TokenCountStream<S> {
461 fn new(inner: S, max_tokens: u64) -> Self {
462 Self {
463 inner,
464 max_tokens,
465 total: 0,
466 encoder: tiktoken_rs::o200k_base()
468 .expect("o200k_base BPE data is embedded and always loads"),
469 finished: false,
470 }
471 }
472}
473
474impl<S, I> Stream for TokenCountStream<S>
475where
476 S: Stream<Item = Result<I, Error>> + Unpin,
477 I: serde::Serialize,
478{
479 type Item = Result<I, Error>;
480
481 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
482 let this = self.get_mut();
483 if this.finished {
484 return Poll::Ready(None);
485 }
486 match Pin::new(&mut this.inner).poll_next(cx) {
487 Poll::Ready(Some(Ok(item))) => {
488 let json = match serde_json::to_string(&item) {
490 Ok(s) => s,
491 Err(e) => {
492 this.finished = true;
493 return Poll::Ready(Some(Err(Error::InlineJson(e))));
494 }
495 };
496 this.total += this.encoder.encode_with_special_tokens(&json).len() as u64;
497 if this.total > this.max_tokens {
498 this.finished = true;
499 return Poll::Ready(Some(Err(Error::TokenBudgetExceeded {
500 limit: this.max_tokens,
501 actual: this.total,
502 })));
503 }
504 Poll::Ready(Some(Ok(item)))
505 }
506 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
507 Poll::Ready(None) => {
508 this.finished = true;
509 Poll::Ready(None)
510 }
511 Poll::Pending => Poll::Pending,
512 }
513 }
514}
515
516struct TimeoutStream<S> {
525 inner: S,
526 timeout_seconds: u64,
527 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
529 finished: bool,
531}
532
533impl<S> TimeoutStream<S> {
534 fn new(inner: S, timeout_seconds: u64) -> Self {
535 Self {
536 inner,
537 timeout_seconds,
538 deadline: None,
539 finished: false,
540 }
541 }
542}
543
544impl<S, I> Stream for TimeoutStream<S>
545where
546 S: Stream<Item = Result<I, Error>> + Unpin,
547{
548 type Item = Result<I, Error>;
549
550 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
551 let this = self.get_mut();
552 if this.finished {
553 return Poll::Ready(None);
554 }
555 let secs = this.timeout_seconds;
556 let deadline = this.deadline.get_or_insert_with(|| {
557 Box::pin(tokio::time::sleep(std::time::Duration::from_secs(secs)))
558 });
559 if deadline.as_mut().poll(cx).is_ready() {
561 this.finished = true;
562 return Poll::Ready(Some(Err(Error::Timeout {
563 timeout_seconds: secs,
564 })));
565 }
566 match Pin::new(&mut this.inner).poll_next(cx) {
567 Poll::Ready(Some(item)) => Poll::Ready(Some(item)),
568 Poll::Ready(None) => {
569 this.finished = true;
570 Poll::Ready(None)
571 }
572 Poll::Pending => Poll::Pending,
573 }
574 }
575}