objectiveai_cli/command/agents/
message.rs1use futures::StreamExt;
26use objectiveai_sdk::agent::completions::message::RichContent;
27use objectiveai_sdk::cli::command::agents::message::{Request, RequestMessage, Response};
28use objectiveai_sdk::cli::command::agents::selector::{AgentRef, AgentSelector};
29use objectiveai_sdk::cli::command::agents::spawn as spawn_sdk;
30use objectiveai_sdk::cli::command::{BinaryExecutor, CommandExecutor};
31use objectiveai_sdk::lockfile::LockClaim;
32
33use crate::context::Context;
34use crate::error::Error;
35
36pub async fn execute(ctx: &Context, request: Request) -> Result<Response, Error> {
37 let Request {
38 agent,
39 message,
40 dangerous_advanced,
41 ..
42 } = request;
43 let seed = dangerous_advanced.as_ref().and_then(|a| a.seed);
44
45 let content = resolve_message(ctx, message).await?;
47
48 let state_dir = ctx.filesystem.state_dir();
49 let route = match agent {
50 AgentSelector::Ref { agent } => {
51 let resolved = super::spawn::resolve_agent_ref(ctx, agent).await?;
54 Route::Ref {
55 child: AgentSelector::Ref {
56 agent: AgentRef::Resolved(resolved),
57 },
58 }
59 }
60 AgentSelector::Instance {
61 parent_agent_instance_hierarchy,
62 agent_instance,
63 } => {
64 let parent = parent_agent_instance_hierarchy
65 .as_deref()
66 .unwrap_or(&ctx.config.agent_instance_hierarchy);
67 instance_route(&state_dir, format!("{parent}/{agent_instance}"))
68 }
69 AgentSelector::Tag { agent_tag } => {
70 match crate::db::tags::lookup(ctx.db_client().await?, &agent_tag).await? {
71 crate::db::tags::LookupState::Bound {
72 agent_instance_hierarchy,
73 } => instance_route(&state_dir, agent_instance_hierarchy),
74 crate::db::tags::LookupState::Grouped { .. } => {
75 let (dir, key) = super::locks::agent_tag_lock(&state_dir, &agent_tag);
76 Route::Locked {
77 dir,
78 key,
79 hierarchy: None,
80 tag: Some(agent_tag.clone()),
81 child: AgentSelector::Tag { agent_tag },
82 }
83 }
84 crate::db::tags::LookupState::Absent => {
85 return Err(Error::TagNotFound(agent_tag));
86 }
87 }
88 }
89 };
90
91 match route {
92 Route::Ref { child } => spawn_child(child, content, seed, None, false).await,
93 Route::Locked {
94 dir,
95 key,
96 hierarchy,
97 tag,
98 child,
99 } => {
100 let is_tag = tag.is_some();
101 if let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await {
104 return spawn_locked(child, content, seed, claim, is_tag).await;
105 }
106
107 let queue_id = crate::db::message_queue::enqueue_with_content(
112 ctx.db_client().await?,
113 hierarchy,
114 tag,
115 &ctx.config.agent_instance_hierarchy,
116 None,
117 content.clone(),
118 )
119 .await?;
120 let pool = ctx.db_client().await?.clone();
121 tokio::select! {
122 delivery = crate::db::message_queue::subscribe_delivered(&pool, queue_id) => {
123 delivery?;
124 Ok(Response::Delivered)
125 }
126 claim = objectiveai_sdk::lockfile::wait_acquire(&dir, &key, "") => {
127 let claim = claim.map_err(|e| Error::Lockfile {
128 key: key.clone(),
129 source: e,
130 })?;
131 let _ = crate::db::message_queue::delete_by_id(
135 ctx.db_client().await?,
136 queue_id,
137 &ctx.config.agent_instance_hierarchy,
138 )
139 .await;
140 spawn_locked(child, content, seed, claim, is_tag).await
141 }
142 }
143 }
144 }
145}
146
147enum Route {
149 Ref { child: AgentSelector },
152 Locked {
156 dir: std::path::PathBuf,
157 key: String,
158 hierarchy: Option<String>,
159 tag: Option<String>,
160 child: AgentSelector,
161 },
162}
163
164fn instance_route(state_dir: &std::path::Path, hierarchy: String) -> Route {
169 let (dir, key) = super::locks::agent_instance_lock(state_dir, &hierarchy);
170 let child = AgentSelector::Instance {
171 parent_agent_instance_hierarchy: Some(
172 crate::db::tags::parent_of(&hierarchy).to_string(),
173 ),
174 agent_instance: crate::db::tags::leaf_of(&hierarchy).to_string(),
175 };
176 Route::Locked {
177 dir,
178 key,
179 hierarchy: Some(hierarchy),
180 tag: None,
181 child,
182 }
183}
184
185async fn spawn_locked(
195 agent: AgentSelector,
196 content: RichContent,
197 seed: Option<i64>,
198 claim: LockClaim,
199 is_tag: bool,
200) -> Result<Response, Error> {
201 if is_tag {
202 let _tag_claim = claim;
203 spawn_child(agent, content, seed, None, true).await
204 } else {
205 spawn_child(agent, content, seed, Some(claim), true).await
206 }
207}
208
209async fn spawn_child(
218 agent: AgentSelector,
219 content: RichContent,
220 seed: Option<i64>,
221 transfer: Option<LockClaim>,
222 skip_lock: bool,
223) -> Result<Response, Error> {
224 let skip_lock = skip_lock.then_some(true);
225 let child_request = spawn_sdk::Request {
226 path_type: spawn_sdk::Path::AgentsSpawn,
227 message: RequestMessage::Inline(content),
228 agent,
229 dangerous_advanced: Some(spawn_sdk::RequestDangerousAdvanced {
230 stream: Some(true),
231 seed,
232 skip_lock,
233 }),
234 base: Default::default(),
235 };
236
237 let exe = std::env::current_exe()
238 .map_err(|e| Error::Spawn("current_exe".into(), e))?;
239 let mut executor = BinaryExecutor::from_path(exe).detach(true);
240 if let Some(claim) = transfer {
241 executor = executor.transfer_lock(claim);
242 }
243 let mut stream = executor
244 .execute::<spawn_sdk::Request, spawn_sdk::ResponseItem>(child_request, None)
245 .await
246 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
247 let first = stream
248 .next()
249 .await
250 .ok_or(Error::EmptyStream)?
251 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
252 match first {
253 spawn_sdk::ResponseItem::Id(agent_instance_hierarchy) => Ok(Response::Id {
254 agent_instance_hierarchy,
255 }),
256 spawn_sdk::ResponseItem::Chunk(_) => Err(Error::Instance(
257 "agents spawn child emitted a chunk before its id".to_string(),
258 )),
259 }
260}
261
262pub async fn resolve_message(
263 ctx: &Context,
264 message: RequestMessage,
265) -> Result<RichContent, Error> {
266 let (simple, inline, file, python_inline, python_file) = match message {
267 RequestMessage::Inline(rich) => return Ok(rich),
268 RequestMessage::Simple(s) => (Some(s), None, None, None, None),
269 RequestMessage::File(p) => (None, None, Some(p), None, None),
270 RequestMessage::PythonInline(code) => (None, None, None, Some(code), None),
271 RequestMessage::PythonFile(p) => (None, None, None, None, Some(p)),
272 };
273 crate::source_resolver::resolve_source(
274 ctx,
275 simple,
276 inline,
277 file,
278 python_inline,
279 python_file,
280 RichContent::Text,
281 )
282 .await
283}
284
285pub mod request_schema {
286 use objectiveai_sdk::cli::command::agents::message as sdk;
287 use objectiveai_sdk::cli::command::agents::message::request_schema::{Request, Response};
288
289 use crate::context::Context;
290 use crate::error::Error;
291
292 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
293 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
294 }
295}
296
297pub mod response_schema {
298 use objectiveai_sdk::cli::command::agents::message as sdk;
299 use objectiveai_sdk::cli::command::agents::message::response_schema::{Request, Response};
300
301 use crate::context::Context;
302 use crate::error::Error;
303
304 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
305 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
306 }
307}