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};
31
32use crate::context::Context;
33use crate::error::Error;
34
35pub async fn execute(ctx: &Context, request: Request) -> Result<Response, Error> {
36 let Request {
37 agent,
38 message,
39 dangerous_advanced,
40 ..
41 } = request;
42 let seed = dangerous_advanced.as_ref().and_then(|a| a.seed);
43
44 let content = resolve_message(ctx, message).await?;
46
47 let state_dir = ctx.filesystem.state_dir();
48 let route = match agent {
49 AgentSelector::Ref { agent } => {
50 let resolved = super::spawn::resolve_agent_ref(ctx, agent).await?;
53 Route::Ref {
54 child: AgentSelector::Ref {
55 agent: AgentRef::Resolved(resolved),
56 },
57 }
58 }
59 AgentSelector::Instance {
60 parent_agent_instance_hierarchy,
61 agent_instance,
62 } => {
63 let parent = parent_agent_instance_hierarchy
64 .as_deref()
65 .unwrap_or(&ctx.config.agent_instance_hierarchy);
66 instance_route(&state_dir, format!("{parent}/{agent_instance}"))
67 }
68 AgentSelector::Tag { agent_tag } => {
69 match crate::db::tags::lookup(ctx.db_client().await?, &agent_tag).await? {
70 crate::db::tags::LookupState::Bound {
71 agent_instance_hierarchy,
72 } => instance_route(&state_dir, agent_instance_hierarchy),
73 crate::db::tags::LookupState::Grouped { tag_group_id, .. } => {
74 let (dir, key) = super::locks::agent_tag_lock(&state_dir, &agent_tag);
75 Route::Locked {
76 dir,
77 key,
78 family: super::locks::Family::Group(tag_group_id),
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, Vec::new()).await,
93 Route::Locked {
94 dir,
95 key,
96 family,
97 hierarchy,
98 tag,
99 child,
100 } => {
101 if let Some(fam) = super::locks::try_acquire_family(
105 ctx.agent_locks(),
106 ctx.db_client().await?,
107 &state_dir,
108 family.clone(),
109 )
110 .await?
111 {
112 return spawn_locked(child, content, seed, fam.into_locks()).await;
113 }
114
115 let queue_id = crate::db::message_queue::enqueue_with_content(
121 ctx.db_client().await?,
122 hierarchy,
123 tag,
124 &ctx.config.agent_instance_hierarchy,
125 None,
126 content.clone(),
127 )
128 .await?;
129 let pool = ctx.db_client().await?.clone();
130 let primary = (dir, key);
131 let mut child = Some(child);
132 let mut content = Some(content);
133 loop {
134 tokio::select! {
135 delivery = crate::db::message_queue::subscribe_delivered(&pool, queue_id) => {
136 delivery?;
137 return Ok(Response::Delivered);
138 }
139 released = objectiveai_sdk::lockfile::wait_released(&primary.0, &primary.1) => {
142 released.map_err(|e| Error::Lockfile {
143 key: primary.1.clone(),
144 source: e,
145 })?;
146 match super::locks::try_acquire_family(
147 ctx.agent_locks(),
148 ctx.db_client().await?,
149 &state_dir,
150 family.clone(),
151 )
152 .await?
153 {
154 Some(fam) => {
155 let _ = crate::db::message_queue::delete_by_id(
159 ctx.db_client().await?,
160 queue_id,
161 &ctx.config.agent_instance_hierarchy,
162 )
163 .await;
164 return spawn_locked(
165 child.take().expect("child consumed once"),
166 content.take().expect("content consumed once"),
167 seed,
168 fam.into_locks(),
169 )
170 .await;
171 }
172 None => continue,
175 }
176 }
177 }
178 }
179 }
180 }
181}
182
183enum Route {
185 Ref { child: AgentSelector },
188 Locked {
193 dir: std::path::PathBuf,
194 key: String,
195 family: super::locks::Family,
196 hierarchy: Option<String>,
197 tag: Option<String>,
198 child: AgentSelector,
199 },
200}
201
202fn instance_route(state_dir: &std::path::Path, hierarchy: String) -> Route {
207 let (dir, key) = super::locks::agent_instance_lock(state_dir, &hierarchy);
208 let child = AgentSelector::Instance {
209 parent_agent_instance_hierarchy: Some(
210 crate::db::tags::parent_of(&hierarchy).to_string(),
211 ),
212 agent_instance: crate::db::tags::leaf_of(&hierarchy).to_string(),
213 };
214 Route::Locked {
215 dir,
216 key,
217 family: super::locks::Family::Hierarchy(hierarchy.clone()),
218 hierarchy: Some(hierarchy),
219 tag: None,
220 child,
221 }
222}
223
224async fn spawn_locked(
233 agent: AgentSelector,
234 content: RichContent,
235 seed: Option<i64>,
236 family: Vec<super::locks::AgentLock>,
237) -> Result<Response, Error> {
238 spawn_child(agent, content, seed, family).await
239}
240
241async fn spawn_child(
250 agent: AgentSelector,
251 content: RichContent,
252 seed: Option<i64>,
253 transfer: Vec<super::locks::AgentLock>,
254) -> Result<Response, Error> {
255 let child_request = spawn_sdk::Request {
256 path_type: spawn_sdk::Path::AgentsSpawn,
257 message: RequestMessage::Inline(content),
258 agent,
259 dangerous_advanced: Some(spawn_sdk::RequestDangerousAdvanced {
260 stream: Some(true),
261 seed,
262 }),
263 base: Default::default(),
264 };
265
266 let exe = std::env::current_exe()
267 .map_err(|e| Error::Spawn("current_exe".into(), e))?;
268 let mut executor = BinaryExecutor::from_path(exe).detach(true);
269 let mut transfer = transfer;
276 let claims: Vec<_> = transfer
277 .iter_mut()
278 .filter_map(|lock| lock.take_claim())
279 .collect();
280 if !claims.is_empty() {
281 executor = executor.transfer_locks(claims);
282 }
283 let mut stream = executor
284 .execute::<spawn_sdk::Request, spawn_sdk::ResponseItem>(child_request, None)
285 .await
286 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
287 let first = stream
288 .next()
289 .await
290 .ok_or(Error::EmptyStream)?
291 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
292 match first {
293 spawn_sdk::ResponseItem::Id(agent_instance_hierarchy) => Ok(Response::Id {
294 agent_instance_hierarchy,
295 }),
296 spawn_sdk::ResponseItem::Chunk(_) => Err(Error::Instance(
297 "agents spawn child emitted a chunk before its id".to_string(),
298 )),
299 }
300}
301
302pub async fn resolve_message(
303 ctx: &Context,
304 message: RequestMessage,
305) -> Result<RichContent, Error> {
306 let (simple, inline, file, python_inline, python_file) = match message {
307 RequestMessage::Inline(rich) => return Ok(rich),
308 RequestMessage::Simple(s) => (Some(s), None, None, None, None),
309 RequestMessage::File(p) => (None, None, Some(p), None, None),
310 RequestMessage::PythonInline(code) => (None, None, None, Some(code), None),
311 RequestMessage::PythonFile(p) => (None, None, None, None, Some(p)),
312 };
313 crate::source_resolver::resolve_source(
314 ctx,
315 simple,
316 inline,
317 file,
318 python_inline,
319 python_file,
320 RichContent::Text,
321 )
322 .await
323}
324
325pub mod request_schema {
326 use objectiveai_sdk::cli::command::agents::message as sdk;
327 use objectiveai_sdk::cli::command::agents::message::request_schema::{Request, Response};
328
329 use crate::context::Context;
330 use crate::error::Error;
331
332 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
333 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
334 }
335}
336
337pub mod response_schema {
338 use objectiveai_sdk::cli::command::agents::message as sdk;
339 use objectiveai_sdk::cli::command::agents::message::response_schema::{Request, Response};
340
341 use crate::context::Context;
342 use crate::error::Error;
343
344 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
345 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
346 }
347}