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,
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 = request.into_command();
123 let sdk_request = parse_request(&argv).map_err(|e| match e {
124 objectiveai_sdk::cli::command::ParseError::Clap(e) => Error::ClapParse(e),
125 objectiveai_sdk::cli::command::ParseError::FromArgs(e) => Error::FromArgs(e),
126 })?;
127 let ctx = self.resolve_ctx(agent_arguments).into_owned();
130 let transform_ctx =
135 matches!(transform, Some(Transform::Python(_))).then(|| ctx.clone());
136
137 let source = futures::stream::once(async move {
145 crate::command::command::execute(&ctx, sdk_request).await
146 })
147 .try_flatten();
148 let source: Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>> =
149 Box::pin(source);
150
151 let mut stream: Self::Stream<T> = Box::pin(ConvertStream::new(source));
157
158 if let Some(transform) = transform {
164 match transform {
165 Transform::Python(code) => {
166 let ctx = transform_ctx.expect("cloned whenever a python transform is present");
167 stream = Box::pin(PythonTransformStream::new(stream, ctx, code));
168 }
169 Transform::Jq(filter) => {
170 stream = Box::pin(JqTransformStream::new(stream, filter));
171 }
172 }
173 }
174
175 if let Some(max_tokens) = max_tokens {
179 stream = Box::pin(TokenCountStream::new(stream, max_tokens));
180 }
181
182 if let Some(timeout_seconds) = timeout {
186 stream = Box::pin(TimeoutStream::new(stream, timeout_seconds));
187 }
188
189 Ok(stream)
190 }
191
192 async fn execute_one<R, T>(
193 &self,
194 request: R,
195 agent_arguments: Option<&AgentArguments>,
196 ) -> Result<T, Self::Error>
197 where
198 R: CommandRequest + Send,
199 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
200 {
201 let mut stream: Self::Stream<T> =
202 self.execute::<R, T>(request, agent_arguments).await?;
203 match stream.next().await {
204 Some(item) => item,
205 None => Err(Error::EmptyStream),
206 }
207 }
208}
209
210struct ConvertStream<S, T> {
219 inner: S,
220 identity: bool,
222 _marker: PhantomData<fn() -> T>,
223}
224
225impl<S, T: 'static> ConvertStream<S, T> {
226 fn new(inner: S) -> Self {
227 Self {
228 inner,
229 identity: TypeId::of::<T>() == TypeId::of::<ResponseItem>(),
230 _marker: PhantomData,
231 }
232 }
233}
234
235impl<S, T> Stream for ConvertStream<S, T>
236where
237 S: Stream<Item = Result<ResponseItem, Error>> + Unpin,
238 T: serde::de::DeserializeOwned + 'static,
239{
240 type Item = Result<T, Error>;
241
242 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
243 let this = self.get_mut();
244 match Pin::new(&mut this.inner).poll_next(cx) {
245 Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(convert_item::<T>(item, this.identity))),
246 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
247 Poll::Ready(None) => Poll::Ready(None),
248 Poll::Pending => Poll::Pending,
249 }
250 }
251}
252
253fn convert_item<T: serde::de::DeserializeOwned + 'static>(
255 item: ResponseItem,
256 identity: bool,
257) -> Result<T, Error> {
258 if identity {
259 let mut slot = Some(item);
260 Ok((&mut slot as &mut dyn Any)
261 .downcast_mut::<Option<T>>()
262 .and_then(Option::take)
263 .expect("identity flag guarantees T == ResponseItem"))
264 } else {
265 let value = serde_json::to_value(item).map_err(Error::InlineJson)?;
266 extract_leaf::<T>(value).map_err(Error::InlineJson)
267 }
268}
269
270struct PythonTransformStream<S, T> {
279 inner: S,
280 ctx: Context,
281 code: Arc<str>,
282 pending: Option<Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, Error>> + Send>>>,
283 _marker: PhantomData<fn() -> T>,
284}
285
286impl<S, T> PythonTransformStream<S, T> {
287 fn new(inner: S, ctx: Context, code: String) -> Self {
288 Self {
289 inner,
290 ctx,
291 code: Arc::from(code),
292 pending: None,
293 _marker: PhantomData,
294 }
295 }
296}
297
298impl<S, T> Stream for PythonTransformStream<S, T>
299where
300 S: Stream<Item = Result<T, Error>> + Unpin,
301 T: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
302{
303 type Item = Result<T, Error>;
304
305 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
306 let this = self.get_mut();
307 loop {
308 if let Some(fut) = this.pending.as_mut() {
313 match fut.as_mut().poll(cx) {
314 Poll::Ready(Ok(Some(value))) => {
315 this.pending = None;
316 if !value.is_null() {
317 return Poll::Ready(Some(
318 serde_json::from_value::<T>(value).map_err(Error::InlineJson),
319 ));
320 }
321 }
323 Poll::Ready(Ok(None)) => this.pending = None,
325 Poll::Ready(Err(e)) => {
326 this.pending = None;
327 return Poll::Ready(Some(Err(e)));
328 }
329 Poll::Pending => return Poll::Pending,
330 }
331 }
332 match Pin::new(&mut this.inner).poll_next(cx) {
334 Poll::Ready(Some(Ok(item))) => {
335 let ctx = this.ctx.clone();
336 let code = Arc::clone(&this.code);
337 this.pending = Some(Box::pin(async move {
338 transform_item::<T>(&ctx, &code, item).await
339 }));
340 }
342 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
343 Poll::Ready(None) => return Poll::Ready(None),
344 Poll::Pending => return Poll::Pending,
345 }
346 }
347 }
348}
349
350async fn transform_item<I: serde::Serialize>(
357 ctx: &Context,
358 code: &str,
359 item: I,
360) -> Result<Option<serde_json::Value>, Error> {
361 ctx.python().await?.exec_code(code, Some(item)).await
362}
363
364struct JqTransformStream<S, T> {
375 inner: S,
376 filter: Arc<str>,
377 _marker: PhantomData<fn() -> T>,
378}
379
380impl<S, T> JqTransformStream<S, T> {
381 fn new(inner: S, filter: String) -> Self {
382 Self {
383 inner,
384 filter: Arc::from(filter),
385 _marker: PhantomData,
386 }
387 }
388}
389
390impl<S, T> Stream for JqTransformStream<S, T>
391where
392 S: Stream<Item = Result<T, Error>> + Unpin,
393 T: serde::Serialize + serde::de::DeserializeOwned,
394{
395 type Item = Result<T, Error>;
396
397 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
398 let this = self.get_mut();
399 loop {
400 match Pin::new(&mut this.inner).poll_next(cx) {
401 Poll::Ready(Some(Ok(item))) => match jq_apply::<T>(&item, &this.filter) {
402 Ok(Some(t)) => return Poll::Ready(Some(Ok(t))),
403 Ok(None) => {}
405 Err(e) => return Poll::Ready(Some(Err(e))),
406 },
407 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
408 Poll::Ready(None) => return Poll::Ready(None),
409 Poll::Pending => return Poll::Pending,
410 }
411 }
412 }
413}
414
415fn jq_apply<T: serde::Serialize + serde::de::DeserializeOwned>(
419 item: &T,
420 filter: &str,
421) -> Result<Option<T>, Error> {
422 let mut results = crate::filesystem::run_jq(item, filter).map_err(Error::Filesystem)?;
423 let value = match results.len() {
424 0 => serde_json::Value::Null,
425 1 => results.remove(0),
426 _ => serde_json::Value::Array(results),
427 };
428 if value.is_null() {
429 Ok(None)
430 } else {
431 serde_json::from_value::<T>(value)
432 .map(Some)
433 .map_err(Error::InlineJson)
434 }
435}
436
437struct TokenCountStream<S> {
446 inner: S,
447 max_tokens: u64,
448 total: u64,
449 encoder: tiktoken_rs::CoreBPE,
450 finished: bool,
452}
453
454impl<S> TokenCountStream<S> {
455 fn new(inner: S, max_tokens: u64) -> Self {
456 Self {
457 inner,
458 max_tokens,
459 total: 0,
460 encoder: tiktoken_rs::o200k_base()
462 .expect("o200k_base BPE data is embedded and always loads"),
463 finished: false,
464 }
465 }
466}
467
468impl<S, I> Stream for TokenCountStream<S>
469where
470 S: Stream<Item = Result<I, Error>> + Unpin,
471 I: serde::Serialize,
472{
473 type Item = Result<I, Error>;
474
475 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
476 let this = self.get_mut();
477 if this.finished {
478 return Poll::Ready(None);
479 }
480 match Pin::new(&mut this.inner).poll_next(cx) {
481 Poll::Ready(Some(Ok(item))) => {
482 let json = match serde_json::to_string(&item) {
484 Ok(s) => s,
485 Err(e) => {
486 this.finished = true;
487 return Poll::Ready(Some(Err(Error::InlineJson(e))));
488 }
489 };
490 this.total += this.encoder.encode_with_special_tokens(&json).len() as u64;
491 if this.total > this.max_tokens {
492 this.finished = true;
493 return Poll::Ready(Some(Err(Error::TokenBudgetExceeded {
494 limit: this.max_tokens,
495 actual: this.total,
496 })));
497 }
498 Poll::Ready(Some(Ok(item)))
499 }
500 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
501 Poll::Ready(None) => {
502 this.finished = true;
503 Poll::Ready(None)
504 }
505 Poll::Pending => Poll::Pending,
506 }
507 }
508}
509
510struct TimeoutStream<S> {
519 inner: S,
520 timeout_seconds: u64,
521 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
523 finished: bool,
525}
526
527impl<S> TimeoutStream<S> {
528 fn new(inner: S, timeout_seconds: u64) -> Self {
529 Self {
530 inner,
531 timeout_seconds,
532 deadline: None,
533 finished: false,
534 }
535 }
536}
537
538impl<S, I> Stream for TimeoutStream<S>
539where
540 S: Stream<Item = Result<I, Error>> + Unpin,
541{
542 type Item = Result<I, Error>;
543
544 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
545 let this = self.get_mut();
546 if this.finished {
547 return Poll::Ready(None);
548 }
549 let secs = this.timeout_seconds;
550 let deadline = this.deadline.get_or_insert_with(|| {
551 Box::pin(tokio::time::sleep(std::time::Duration::from_secs(secs)))
552 });
553 if deadline.as_mut().poll(cx).is_ready() {
555 this.finished = true;
556 return Poll::Ready(Some(Err(Error::Timeout {
557 timeout_seconds: secs,
558 })));
559 }
560 match Pin::new(&mut this.inner).poll_next(cx) {
561 Poll::Ready(Some(item)) => Poll::Ready(Some(item)),
562 Poll::Ready(None) => {
563 this.finished = true;
564 Poll::Ready(None)
565 }
566 Poll::Pending => Poll::Pending,
567 }
568 }
569}