objectiveai_cli/command/agents/queue/
deliver.rs1use std::collections::HashSet;
45use std::pin::Pin;
46
47use futures::{Stream, StreamExt};
48use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
49use objectiveai_sdk::cli::command::ResponseItem as RootResponseItem;
50use objectiveai_sdk::cli::command::agents::ResponseItem as AgentsResponseItem;
51use objectiveai_sdk::cli::command::agents::queue::deliver::{
52 AgentActiveResponseItem, AgentActiveType, AgentSpawnedResponseItem, AgentSpawnedType,
53 AllAgentsActive, Request, RequestDangerousAdvanced, ResponseItem, TagActiveResponseItem,
54 TagActiveType, TagSpawnedResponseItem, TagSpawnedType, ValueResponseItem,
55};
56use objectiveai_sdk::cli::command::agents::spawn::ResponseItem as SpawnResponseItem;
57use objectiveai_sdk::cli::command::{BinaryExecutor, CommandExecutor};
58
59use crate::context::Context;
60use crate::db;
61use crate::error::Error;
62use crate::websockets::agent_registry::AgentInstanceRegistry;
63
64type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
65
66type TaggedStream =
70 Pin<Box<dyn Stream<Item = (usize, Result<ResponseItem, Error>)> + Send>>;
71
72pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
73 if request
74 .dangerous_advanced
75 .as_ref()
76 .and_then(|adv| adv.stream_spawns)
77 == Some(true)
78 {
79 execute_streaming(ctx, request).await
80 } else {
81 execute_detached(request).await
82 }
83}
84
85async fn execute_detached(request: Request) -> Result<ItemStream, Error> {
95 let mut child_request = request;
96 match child_request.dangerous_advanced.as_mut() {
97 Some(adv) => adv.stream_spawns = Some(true),
98 None => {
99 child_request.dangerous_advanced = Some(RequestDangerousAdvanced {
100 stream_spawns: Some(true),
101 })
102 }
103 }
104 crate::command::reexec::strip_inherited(&mut child_request.base);
106
107 let exe = std::env::current_exe()
108 .map_err(|e| Error::Spawn("current_exe".into(), e))?;
109 let executor = BinaryExecutor::from_path(exe).detach(true);
110
111 let mut stream = executor
112 .execute::<Request, ResponseItem>(child_request, None)
113 .await
114 .map_err(|e| Error::Instance(format!(
115 "self-respawn for agents queue deliver: {e}"
116 )))?;
117
118 let out = async_stream::stream! {
119 while let Some(item) = stream.next().await {
120 match item {
121 Ok(ResponseItem::Value(_)) => {}
124 Ok(item) => {
125 let done = matches!(item, ResponseItem::AllAgentsActive(_));
126 yield Ok(item);
127 if done {
128 break;
132 }
133 }
134 Err(e) => yield Err(Error::Instance(format!(
135 "self-respawn for agents queue deliver: {e}"
136 ))),
137 }
138 }
139 };
140 Ok(Box::pin(out))
141}
142
143async fn execute_streaming(ctx: &Context, _request: Request) -> Result<ItemStream, Error> {
145 let caller = ctx.config.agent_instance_hierarchy.clone();
149 let targets = db::message_queue::list_delivery_targets(ctx.db_client().await?, &caller).await?;
150 let mut hierarchies: Vec<String> = Vec::new();
151 let mut tags: Vec<String> = Vec::new();
152 for target in targets {
153 match target {
154 db::message_queue::DeliveryTarget::Hierarchy { agent_instance_hierarchy } => {
155 if agent_instance_hierarchy != caller
156 && !hierarchies.contains(&agent_instance_hierarchy)
157 {
158 hierarchies.push(agent_instance_hierarchy);
159 }
160 }
161 db::message_queue::DeliveryTarget::GroupedTag { agent_tag } => {
162 if !tags.contains(&agent_tag) {
163 tags.push(agent_tag);
164 }
165 }
166 }
167 }
168
169 let n = hierarchies.len() + tags.len();
170 let mut select_all = futures::stream::SelectAll::new();
171 let mut idx = 0usize;
172 for hierarchy in hierarchies {
173 let i = idx;
174 idx += 1;
175 let tagged = deliver_one_hierarchy(ctx.clone(), hierarchy).map(move |item| (i, item));
176 select_all.push(Box::pin(tagged) as TaggedStream);
177 }
178 for tag in tags {
179 let i = idx;
180 idx += 1;
181 let tagged = deliver_one_tag(ctx.clone(), tag).map(move |item| (i, item));
182 select_all.push(Box::pin(tagged) as TaggedStream);
183 }
184
185 let out = async_stream::stream! {
186 if n == 0 {
187 yield Ok(ResponseItem::AllAgentsActive(AllAgentsActive::AllAgentsActive));
188 return;
189 }
190 let mut seen: HashSet<usize> = HashSet::new();
191 let mut merged = select_all;
192 while let Some((idx, item)) = merged.next().await {
193 let first = seen.insert(idx);
194 yield item;
195 if first && seen.len() == n {
200 yield Ok(ResponseItem::AllAgentsActive(AllAgentsActive::AllAgentsActive));
201 }
202 }
203 };
204 Ok(Box::pin(out))
205}
206
207fn deliver_one_hierarchy(
215 ctx: Context,
216 hierarchy: String,
217) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
218 async_stream::stream! {
219 let state_dir = ctx.filesystem.state_dir();
220 let (dir, key) =
221 crate::command::agents::locks::agent_instance_lock(&state_dir, &hierarchy);
222 let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await else {
223 yield Ok(ResponseItem::AgentActive(AgentActiveResponseItem {
224 r#type: AgentActiveType::AgentActive,
225 agent_instance_hierarchy: hierarchy,
226 }));
227 return;
228 };
229
230 let pool = match ctx.db_client().await {
231 Ok(pool) => pool,
232 Err(e) => {
233 let _ = claim.release();
234 yield Err(e);
235 return;
236 }
237 };
238 let lookup = match crate::db::logs::lookup_session(pool, &hierarchy).await {
239 Ok(Some(lookup)) => lookup,
240 Ok(None) => {
241 let _ = claim.release();
244 yield Err(Error::AgentNoPriorRequest {
245 agent_instance_hierarchy: hierarchy,
246 });
247 return;
248 }
249 Err(e) => {
250 let _ = claim.release();
251 yield Err(e.into());
252 return;
253 }
254 };
255
256 let mut registry = AgentInstanceRegistry::new(state_dir);
257 registry.preseed(hierarchy.clone(), claim);
258
259 yield Ok(ResponseItem::AgentSpawned(AgentSpawnedResponseItem {
260 r#type: AgentSpawnedType::AgentSpawned,
261 agent_instance_hierarchy: hierarchy.clone(),
262 }));
263
264 let params = AgentCompletionCreateParams {
268 messages: Vec::new(),
269 provider: None,
270 agent: lookup.agent,
271 response_format: None,
272 seed: None,
273 stream: Some(true),
274 continuation: lookup.continuation,
275 };
276 let inner = crate::command::agents::spawn::run_multi_pass(
277 ctx.clone(),
278 params,
279 None,
280 registry,
281 );
282 let mut inner = Box::pin(inner);
283 while let Some(item) = inner.next().await {
284 match item {
285 Ok(spawn_item) => {
286 yield Ok(ResponseItem::Value(ValueResponseItem {
287 agent_instance_hierarchy: hierarchy.clone(),
288 value: Box::new(RootResponseItem::Agents(
289 AgentsResponseItem::Spawn(spawn_item),
290 )),
291 }));
292 }
293 Err(e) => yield Err(e),
294 }
295 }
296 }
297}
298
299fn deliver_one_tag(
310 ctx: Context,
311 agent_tag: String,
312) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
313 async_stream::stream! {
314 let state_dir = ctx.filesystem.state_dir();
315 let (dir, key) =
316 crate::command::agents::locks::agent_tag_lock(&state_dir, &agent_tag);
317 let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await else {
318 yield Ok(ResponseItem::TagActive(TagActiveResponseItem {
319 r#type: TagActiveType::TagActive,
320 agent_tag,
321 }));
322 return;
323 };
324
325 let pool = match ctx.db_client().await {
328 Ok(pool) => pool,
329 Err(e) => {
330 let _ = claim.release();
331 yield Err(e);
332 return;
333 }
334 };
335 let agent = match crate::db::tags::lookup(pool, &agent_tag).await {
336 Ok(crate::db::tags::LookupState::Grouped { agent_spec, .. }) => {
337 agent_spec
338 }
339 Ok(crate::db::tags::LookupState::Bound { agent_instance_hierarchy }) => {
340 let _ = claim.release();
343 let mut inner = Box::pin(deliver_one_hierarchy(
344 ctx.clone(),
345 agent_instance_hierarchy,
346 ));
347 while let Some(item) = inner.next().await {
348 yield item;
349 }
350 return;
351 }
352 Ok(crate::db::tags::LookupState::Absent) => {
353 let _ = claim.release();
354 yield Err(Error::TagNotFound(agent_tag));
355 return;
356 }
357 Err(e) => {
358 let _ = claim.release();
359 yield Err(e.into());
360 return;
361 }
362 };
363
364 let mut registry = AgentInstanceRegistry::new(state_dir);
365 registry.hold_tag_claim(claim);
366
367 yield Ok(ResponseItem::TagSpawned(TagSpawnedResponseItem {
368 r#type: TagSpawnedType::TagSpawned,
369 agent_tag: agent_tag.clone(),
370 }));
371
372 let params = AgentCompletionCreateParams {
377 messages: Vec::new(),
378 provider: None,
379 agent,
380 response_format: None,
381 seed: None,
382 stream: Some(true),
383 continuation: None,
384 };
385 let inner = crate::command::agents::spawn::run_multi_pass(
386 ctx.clone(),
387 params,
388 Some(agent_tag),
389 registry,
390 );
391 let mut inner = Box::pin(inner);
392 let mut minted: Option<String> = None;
396 while let Some(item) = inner.next().await {
397 match item {
398 Ok(spawn_item) => {
399 if let SpawnResponseItem::Id(id) = &spawn_item {
400 minted = Some(id.clone());
401 }
402 let aih = minted.clone().unwrap_or_default();
403 yield Ok(ResponseItem::Value(ValueResponseItem {
404 agent_instance_hierarchy: aih,
405 value: Box::new(RootResponseItem::Agents(
406 AgentsResponseItem::Spawn(spawn_item),
407 )),
408 }));
409 }
410 Err(e) => yield Err(e),
411 }
412 }
413 }
414}
415
416pub mod request_schema {
417 use objectiveai_sdk::cli::command::agents::queue::deliver as sdk;
418 use objectiveai_sdk::cli::command::agents::queue::deliver::request_schema::{
419 Request, Response,
420 };
421
422 use crate::context::Context;
423 use crate::error::Error;
424
425 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
426 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
427 }
428}
429
430pub mod response_schema {
431 use objectiveai_sdk::cli::command::agents::queue::deliver as sdk;
432 use objectiveai_sdk::cli::command::agents::queue::deliver::response_schema::{
433 Request, Response,
434 };
435
436 use crate::context::Context;
437 use crate::error::Error;
438
439 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
440 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
441 }
442}