objectiveai_cli/command/tools/
run.rs1use std::pin::Pin;
9use std::process::Stdio;
10
11use futures::Stream;
12use objectiveai_sdk::cli::command::tools::run::{Request, ResponseItem};
13use objectiveai_sdk::cli::{Error as CliError, ErrorType};
14use tokio::process::Command;
15
16use crate::child_io::{PipeEvent, spawn_pipe_reader};
17use crate::context::Context;
18use crate::error::Error;
19
20type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
21
22pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
23 let coord = format!("{}/{}/{}", request.owner, request.name, request.version);
24 let (exec, cwd) = ctx
25 .filesystem
26 .resolve_tool(&request.owner, &request.name, &request.version)
27 .await
28 .ok_or_else(|| Error::ToolNotFound(coord.clone()))?;
29
30 let mut argv = exec;
36 argv.extend(request.args);
37 let mut argv = argv.into_iter();
38 let program = argv.next().ok_or_else(|| {
39 Error::ToolNotFound(format!("{coord} (empty exec)"))
40 })?;
41
42 let program = crate::spawn::resolve_program(program, &cwd);
43
44 let state_dir = ctx
48 .filesystem
49 .state_dir()
50 .join("tools")
51 .join(&request.owner)
52 .join(&request.name)
53 .join(&request.version);
54 tokio::fs::create_dir_all(&state_dir)
55 .await
56 .map_err(Error::ToolSpawn)?;
57
58 let postgres_url = crate::db::compartment::ensure(
64 ctx.db_handle().await?,
65 crate::db::compartment::Kind::Tool,
66 &request.owner,
67 &request.name,
68 &request.version,
69 )
70 .await?;
71
72 let mut cmd = Command::new(&program);
73 cmd.args(argv)
74 .current_dir(&cwd)
75 .env("OBJECTIVEAI_STATE_DIR", &state_dir)
76 .env("OBJECTIVEAI_POSTGRES_URL", postgres_url)
77 .stdin(Stdio::null())
78 .stdout(Stdio::piped())
79 .stderr(Stdio::piped());
80 crate::spawn::apply_config_env(&mut cmd, &ctx.config);
81
82 let mut child = cmd.spawn().map_err(Error::ToolSpawn)?;
83 let stdout = child.stdout.take().expect("stdout was piped");
84 let stderr = child.stderr.take().expect("stderr was piped");
85
86 let mut events = spawn_pipe_reader(stdout, stderr);
87
88 let stream = async_stream::stream! {
89 while let Some(event) = events.recv().await {
90 match event {
91 PipeEvent::Stdout(line) => {
92 yield Ok(ResponseItem::Stdout(line));
93 }
94 PipeEvent::Stderr(line) => {
95 yield Ok(ResponseItem::Stderr(CliError {
96 r#type: ErrorType::Error,
97 level: None,
98 fatal: None,
99 message: serde_json::Value::String(line),
100 }));
101 }
102 PipeEvent::StdoutEof | PipeEvent::StderrEof => {}
103 PipeEvent::StdoutErr(e) | PipeEvent::StderrErr(e) => {
104 yield Err(Error::ToolRead(e));
105 return;
106 }
107 }
108 }
109 match child.wait().await {
114 Ok(status) if status.success() => {}
115 Ok(status) => {
116 yield Err(Error::ToolExit(status.code().unwrap_or(1)));
117 }
118 Err(e) => yield Err(Error::ToolRead(e)),
119 }
120 };
121
122 Ok(Box::pin(stream))
123}
124
125pub mod request_schema {
126 use objectiveai_sdk::cli::command::tools::run as sdk;
127 use objectiveai_sdk::cli::command::tools::run::request_schema::{Request, Response};
128
129 use crate::context::Context;
130 use crate::error::Error;
131
132 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
133 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
134 }
135}
136
137pub mod response_schema {
138 use objectiveai_sdk::cli::command::tools::run as sdk;
139 use objectiveai_sdk::cli::command::tools::run::response_schema::{Request, Response};
140
141 use crate::context::Context;
142 use crate::error::Error;
143
144 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
145 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
146 }
147}