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();
151 let targets = db::message_queue::list_delivery_targets(
152 ctx.db_client().await?,
153 &caller,
154 request.keys.as_deref().unwrap_or(&[]),
155 )
156 .await?;
157 let mut hierarchies: Vec<String> = Vec::new();
158 let mut tags: Vec<String> = Vec::new();
159 for target in targets {
160 match target {
161 db::message_queue::DeliveryTarget::Hierarchy { agent_instance_hierarchy } => {
162 if agent_instance_hierarchy != caller
163 && !hierarchies.contains(&agent_instance_hierarchy)
164 {
165 hierarchies.push(agent_instance_hierarchy);
166 }
167 }
168 db::message_queue::DeliveryTarget::GroupedTag { agent_tag } => {
169 if !tags.contains(&agent_tag) {
170 tags.push(agent_tag);
171 }
172 }
173 }
174 }
175
176 let n = hierarchies.len() + tags.len();
177 let mut select_all = futures::stream::SelectAll::new();
178 let mut idx = 0usize;
179 for hierarchy in hierarchies {
180 let i = idx;
181 idx += 1;
182 let tagged = deliver_one_hierarchy(ctx.clone(), hierarchy).map(move |item| (i, item));
183 select_all.push(Box::pin(tagged) as TaggedStream);
184 }
185 for tag in tags {
186 let i = idx;
187 idx += 1;
188 let tagged = deliver_one_tag(ctx.clone(), tag).map(move |item| (i, item));
189 select_all.push(Box::pin(tagged) as TaggedStream);
190 }
191
192 let out = async_stream::stream! {
193 if n == 0 {
194 yield Ok(ResponseItem::AllAgentsActive(AllAgentsActive::AllAgentsActive));
195 return;
196 }
197 let mut seen: HashSet<usize> = HashSet::new();
198 let mut merged = select_all;
199 while let Some((idx, item)) = merged.next().await {
200 let first = seen.insert(idx);
201 yield item;
202 if first && seen.len() == n {
207 yield Ok(ResponseItem::AllAgentsActive(AllAgentsActive::AllAgentsActive));
208 }
209 }
210 };
211 Ok(Box::pin(out))
212}
213
214fn deliver_one_hierarchy(
222 ctx: Context,
223 hierarchy: String,
224) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
225 async_stream::stream! {
226 let state_dir = ctx.filesystem.state_dir();
227 let (dir, key) =
228 crate::command::agents::locks::agent_instance_lock(&state_dir, &hierarchy);
229 let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await else {
230 yield Ok(ResponseItem::AgentActive(AgentActiveResponseItem {
231 r#type: AgentActiveType::AgentActive,
232 agent_instance_hierarchy: hierarchy,
233 }));
234 return;
235 };
236
237 let pool = match ctx.db_client().await {
238 Ok(pool) => pool,
239 Err(e) => {
240 let _ = claim.release();
241 yield Err(e);
242 return;
243 }
244 };
245 let lookup = match crate::db::logs::lookup_session(pool, &hierarchy).await {
246 Ok(Some(lookup)) => lookup,
247 Ok(None) => {
248 let _ = claim.release();
251 yield Err(Error::AgentNoPriorRequest {
252 agent_instance_hierarchy: hierarchy,
253 });
254 return;
255 }
256 Err(e) => {
257 let _ = claim.release();
258 yield Err(e.into());
259 return;
260 }
261 };
262
263 let mut registry = AgentInstanceRegistry::new(state_dir);
264 registry.preseed(hierarchy.clone(), claim);
265
266 yield Ok(ResponseItem::AgentSpawned(AgentSpawnedResponseItem {
267 r#type: AgentSpawnedType::AgentSpawned,
268 agent_instance_hierarchy: hierarchy.clone(),
269 }));
270
271 let params = AgentCompletionCreateParams {
275 messages: Vec::new(),
276 provider: None,
277 agent: lookup.agent,
278 response_format: None,
279 seed: None,
280 stream: Some(true),
281 continuation: lookup.continuation,
282 };
283 let inner = crate::command::agents::spawn::run_multi_pass(
284 ctx.clone(),
285 params,
286 None,
287 registry,
288 );
289 let mut inner = Box::pin(inner);
290 while let Some(item) = inner.next().await {
291 match item {
292 Ok(spawn_item) => {
293 yield Ok(ResponseItem::Value(ValueResponseItem {
294 agent_instance_hierarchy: hierarchy.clone(),
295 value: Box::new(RootResponseItem::Agents(
296 AgentsResponseItem::Spawn(spawn_item),
297 )),
298 }));
299 }
300 Err(e) => yield Err(e),
301 }
302 }
303 }
304}
305
306fn deliver_one_tag(
317 ctx: Context,
318 agent_tag: String,
319) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
320 async_stream::stream! {
321 let state_dir = ctx.filesystem.state_dir();
322 let (dir, key) =
323 crate::command::agents::locks::agent_tag_lock(&state_dir, &agent_tag);
324 let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await else {
325 yield Ok(ResponseItem::TagActive(TagActiveResponseItem {
326 r#type: TagActiveType::TagActive,
327 agent_tag,
328 }));
329 return;
330 };
331
332 let pool = match ctx.db_client().await {
335 Ok(pool) => pool,
336 Err(e) => {
337 let _ = claim.release();
338 yield Err(e);
339 return;
340 }
341 };
342 let agent = match crate::db::tags::lookup(pool, &agent_tag).await {
343 Ok(crate::db::tags::LookupState::Grouped { agent_spec, .. }) => {
344 agent_spec
345 }
346 Ok(crate::db::tags::LookupState::Bound { agent_instance_hierarchy }) => {
347 let _ = claim.release();
350 let mut inner = Box::pin(deliver_one_hierarchy(
351 ctx.clone(),
352 agent_instance_hierarchy,
353 ));
354 while let Some(item) = inner.next().await {
355 yield item;
356 }
357 return;
358 }
359 Ok(crate::db::tags::LookupState::Absent) => {
360 let _ = claim.release();
361 yield Err(Error::TagNotFound(agent_tag));
362 return;
363 }
364 Err(e) => {
365 let _ = claim.release();
366 yield Err(e.into());
367 return;
368 }
369 };
370
371 let mut registry = AgentInstanceRegistry::new(state_dir);
372 registry.hold_tag_claim(claim);
373
374 yield Ok(ResponseItem::TagSpawned(TagSpawnedResponseItem {
375 r#type: TagSpawnedType::TagSpawned,
376 agent_tag: agent_tag.clone(),
377 }));
378
379 let params = AgentCompletionCreateParams {
384 messages: Vec::new(),
385 provider: None,
386 agent,
387 response_format: None,
388 seed: None,
389 stream: Some(true),
390 continuation: None,
391 };
392 let inner = crate::command::agents::spawn::run_multi_pass(
393 ctx.clone(),
394 params,
395 Some(agent_tag),
396 registry,
397 );
398 let mut inner = Box::pin(inner);
399 let mut minted: Option<String> = None;
403 while let Some(item) = inner.next().await {
404 match item {
405 Ok(spawn_item) => {
406 if let SpawnResponseItem::Id(id) = &spawn_item {
407 minted = Some(id.clone());
408 }
409 let aih = minted.clone().unwrap_or_default();
410 yield Ok(ResponseItem::Value(ValueResponseItem {
411 agent_instance_hierarchy: aih,
412 value: Box::new(RootResponseItem::Agents(
413 AgentsResponseItem::Spawn(spawn_item),
414 )),
415 }));
416 }
417 Err(e) => yield Err(e),
418 }
419 }
420 }
421}
422
423pub mod request_schema {
424 use objectiveai_sdk::cli::command::agents::queue::deliver as sdk;
425 use objectiveai_sdk::cli::command::agents::queue::deliver::request_schema::{
426 Request, Response,
427 };
428
429 use crate::context::Context;
430 use crate::error::Error;
431
432 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
433 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
434 }
435}
436
437pub mod response_schema {
438 use objectiveai_sdk::cli::command::agents::queue::deliver as sdk;
439 use objectiveai_sdk::cli::command::agents::queue::deliver::response_schema::{
440 Request, Response,
441 };
442
443 use crate::context::Context;
444 use crate::error::Error;
445
446 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
447 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
448 }
449}