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
175 .expect("cloned whenever a python transform is present")
176 .with_no_objectiveai(true);
177 stream = Box::pin(PythonTransformStream::new(stream, ctx, code));
178 }
179 Transform::Jq(filter) => {
180 stream = Box::pin(JqTransformStream::new(stream, filter));
181 }
182 }
183 }
184
185 if let Some(max_tokens) = max_tokens {
189 stream = Box::pin(TokenCountStream::new(stream, max_tokens));
190 }
191
192 if let Some(timeout_seconds) = timeout {
196 stream = Box::pin(TimeoutStream::new(stream, timeout_seconds));
197 }
198
199 Ok(stream)
200 }
201
202 async fn execute_one<R, T>(
203 &self,
204 request: R,
205 agent_arguments: Option<&AgentArguments>,
206 ) -> Result<T, Self::Error>
207 where
208 R: CommandRequest + Send + serde::Serialize,
209 T: CommandResponse + serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
210 {
211 let mut stream: Self::Stream<T> =
212 self.execute::<R, T>(request, agent_arguments).await?;
213 match stream.next().await {
214 Some(item) => item,
215 None => Err(Error::EmptyStream),
216 }
217 }
218}
219
220struct ConvertStream<S, T> {
229 inner: S,
230 identity: bool,
232 _marker: PhantomData<fn() -> T>,
233}
234
235impl<S, T: 'static> ConvertStream<S, T> {
236 fn new(inner: S) -> Self {
237 Self {
238 inner,
239 identity: TypeId::of::<T>() == TypeId::of::<ResponseItem>(),
240 _marker: PhantomData,
241 }
242 }
243}
244
245impl<S, T> Stream for ConvertStream<S, T>
246where
247 S: Stream<Item = Result<ResponseItem, Error>> + Unpin,
248 T: serde::de::DeserializeOwned + 'static,
249{
250 type Item = Result<T, Error>;
251
252 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
253 let this = self.get_mut();
254 match Pin::new(&mut this.inner).poll_next(cx) {
255 Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(convert_item::<T>(item, this.identity))),
256 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
257 Poll::Ready(None) => Poll::Ready(None),
258 Poll::Pending => Poll::Pending,
259 }
260 }
261}
262
263fn convert_item<T: serde::de::DeserializeOwned + 'static>(
265 item: ResponseItem,
266 identity: bool,
267) -> Result<T, Error> {
268 if identity {
269 let mut slot = Some(item);
270 Ok((&mut slot as &mut dyn Any)
271 .downcast_mut::<Option<T>>()
272 .and_then(Option::take)
273 .expect("identity flag guarantees T == ResponseItem"))
274 } else {
275 let value = serde_json::to_value(item).map_err(Error::InlineJson)?;
276 extract_leaf::<T>(value).map_err(Error::InlineJson)
277 }
278}
279
280struct PythonTransformStream<S, T> {
289 inner: S,
290 ctx: Context,
291 code: Arc<str>,
292 pending: Option<Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, Error>> + Send>>>,
293 _marker: PhantomData<fn() -> T>,
294}
295
296impl<S, T> PythonTransformStream<S, T> {
297 fn new(inner: S, ctx: Context, code: String) -> Self {
298 Self {
299 inner,
300 ctx,
301 code: Arc::from(code),
302 pending: None,
303 _marker: PhantomData,
304 }
305 }
306}
307
308impl<S, T> Stream for PythonTransformStream<S, T>
309where
310 S: Stream<Item = Result<T, Error>> + Unpin,
311 T: serde::Serialize + serde::de::DeserializeOwned + Send + 'static,
312{
313 type Item = Result<T, Error>;
314
315 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
316 let this = self.get_mut();
317 loop {
318 if let Some(fut) = this.pending.as_mut() {
323 match fut.as_mut().poll(cx) {
324 Poll::Ready(Ok(Some(value))) => {
325 this.pending = None;
326 if !value.is_null() {
327 return Poll::Ready(Some(
328 serde_json::from_value::<T>(value).map_err(Error::InlineJson),
329 ));
330 }
331 }
333 Poll::Ready(Ok(None)) => this.pending = None,
335 Poll::Ready(Err(e)) => {
336 this.pending = None;
337 return Poll::Ready(Some(Err(e)));
338 }
339 Poll::Pending => return Poll::Pending,
340 }
341 }
342 match Pin::new(&mut this.inner).poll_next(cx) {
344 Poll::Ready(Some(Ok(item))) => {
345 let ctx = this.ctx.clone();
346 let code = Arc::clone(&this.code);
347 this.pending = Some(Box::pin(async move {
348 transform_item::<T>(&ctx, &code, item).await
349 }));
350 }
352 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
353 Poll::Ready(None) => return Poll::Ready(None),
354 Poll::Pending => return Poll::Pending,
355 }
356 }
357 }
358}
359
360async fn transform_item<I: serde::Serialize>(
367 ctx: &Context,
368 code: &str,
369 item: I,
370) -> Result<Option<serde_json::Value>, Error> {
371 ctx.python().await?.exec_code(ctx, code, Some(item)).await
372}
373
374struct JqTransformStream<S, T> {
385 inner: S,
386 filter: Arc<str>,
387 _marker: PhantomData<fn() -> T>,
388}
389
390impl<S, T> JqTransformStream<S, T> {
391 fn new(inner: S, filter: String) -> Self {
392 Self {
393 inner,
394 filter: Arc::from(filter),
395 _marker: PhantomData,
396 }
397 }
398}
399
400impl<S, T> Stream for JqTransformStream<S, T>
401where
402 S: Stream<Item = Result<T, Error>> + Unpin,
403 T: serde::Serialize + serde::de::DeserializeOwned,
404{
405 type Item = Result<T, Error>;
406
407 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
408 let this = self.get_mut();
409 loop {
410 match Pin::new(&mut this.inner).poll_next(cx) {
411 Poll::Ready(Some(Ok(item))) => match jq_apply::<T>(&item, &this.filter) {
412 Ok(Some(t)) => return Poll::Ready(Some(Ok(t))),
413 Ok(None) => {}
415 Err(e) => return Poll::Ready(Some(Err(e))),
416 },
417 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
418 Poll::Ready(None) => return Poll::Ready(None),
419 Poll::Pending => return Poll::Pending,
420 }
421 }
422 }
423}
424
425fn jq_apply<T: serde::Serialize + serde::de::DeserializeOwned>(
429 item: &T,
430 filter: &str,
431) -> Result<Option<T>, Error> {
432 let mut results = crate::filesystem::run_jq(item, filter).map_err(Error::Filesystem)?;
433 let value = match results.len() {
434 0 => serde_json::Value::Null,
435 1 => results.remove(0),
436 _ => serde_json::Value::Array(results),
437 };
438 if value.is_null() {
439 Ok(None)
440 } else {
441 serde_json::from_value::<T>(value)
442 .map(Some)
443 .map_err(Error::InlineJson)
444 }
445}
446
447struct TokenCountStream<S> {
456 inner: S,
457 max_tokens: u64,
458 total: u64,
459 encoder: tiktoken_rs::CoreBPE,
460 finished: bool,
462}
463
464impl<S> TokenCountStream<S> {
465 fn new(inner: S, max_tokens: u64) -> Self {
466 Self {
467 inner,
468 max_tokens,
469 total: 0,
470 encoder: tiktoken_rs::o200k_base()
472 .expect("o200k_base BPE data is embedded and always loads"),
473 finished: false,
474 }
475 }
476}
477
478impl<S, I> Stream for TokenCountStream<S>
479where
480 S: Stream<Item = Result<I, Error>> + Unpin,
481 I: serde::Serialize,
482{
483 type Item = Result<I, Error>;
484
485 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
486 let this = self.get_mut();
487 if this.finished {
488 return Poll::Ready(None);
489 }
490 match Pin::new(&mut this.inner).poll_next(cx) {
491 Poll::Ready(Some(Ok(item))) => {
492 let json = match serde_json::to_string(&item) {
494 Ok(s) => s,
495 Err(e) => {
496 this.finished = true;
497 return Poll::Ready(Some(Err(Error::InlineJson(e))));
498 }
499 };
500 this.total += this.encoder.encode_with_special_tokens(&json).len() as u64;
501 if this.total > this.max_tokens {
502 this.finished = true;
503 return Poll::Ready(Some(Err(Error::TokenBudgetExceeded {
504 limit: this.max_tokens,
505 actual: this.total,
506 })));
507 }
508 Poll::Ready(Some(Ok(item)))
509 }
510 Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
511 Poll::Ready(None) => {
512 this.finished = true;
513 Poll::Ready(None)
514 }
515 Poll::Pending => Poll::Pending,
516 }
517 }
518}
519
520struct TimeoutStream<S> {
529 inner: S,
530 timeout_seconds: u64,
531 deadline: Option<Pin<Box<tokio::time::Sleep>>>,
533 finished: bool,
535}
536
537impl<S> TimeoutStream<S> {
538 fn new(inner: S, timeout_seconds: u64) -> Self {
539 Self {
540 inner,
541 timeout_seconds,
542 deadline: None,
543 finished: false,
544 }
545 }
546}
547
548impl<S, I> Stream for TimeoutStream<S>
549where
550 S: Stream<Item = Result<I, Error>> + Unpin,
551{
552 type Item = Result<I, Error>;
553
554 fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
555 let this = self.get_mut();
556 if this.finished {
557 return Poll::Ready(None);
558 }
559 let secs = this.timeout_seconds;
560 let deadline = this.deadline.get_or_insert_with(|| {
561 Box::pin(tokio::time::sleep(std::time::Duration::from_secs(secs)))
562 });
563 if deadline.as_mut().poll(cx).is_ready() {
565 this.finished = true;
566 return Poll::Ready(Some(Err(Error::Timeout {
567 timeout_seconds: secs,
568 })));
569 }
570 match Pin::new(&mut this.inner).poll_next(cx) {
571 Poll::Ready(Some(item)) => Poll::Ready(Some(item)),
572 Poll::Ready(None) => {
573 this.finished = true;
574 Poll::Ready(None)
575 }
576 Poll::Pending => Poll::Pending,
577 }
578 }
579}