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).await,
93 Route::Locked {
94 dir,
95 key,
96 hierarchy,
97 tag,
98 child,
99 } => {
100 if let Some(claim) = objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await {
103 return spawn_locked(child, content, seed, claim).await;
104 }
105
106 let queue_id = crate::db::message_queue::enqueue_with_content(
111 ctx.db_client().await?,
112 hierarchy,
113 tag,
114 &ctx.config.agent_instance_hierarchy,
115 None,
116 content.clone(),
117 )
118 .await?;
119 let pool = ctx.db_client().await?.clone();
120 tokio::select! {
121 delivery = crate::db::message_queue::subscribe_delivered(&pool, queue_id) => {
122 delivery?;
123 Ok(Response::Delivered)
124 }
125 claim = objectiveai_sdk::lockfile::wait_acquire(&dir, &key, "") => {
126 let claim = claim.map_err(|e| Error::Lockfile {
127 key: key.clone(),
128 source: e,
129 })?;
130 let _ = crate::db::message_queue::delete_by_id(
134 ctx.db_client().await?,
135 queue_id,
136 &ctx.config.agent_instance_hierarchy,
137 )
138 .await;
139 spawn_locked(child, content, seed, claim).await
140 }
141 }
142 }
143 }
144}
145
146enum Route {
148 Ref { child: AgentSelector },
151 Locked {
155 dir: std::path::PathBuf,
156 key: String,
157 hierarchy: Option<String>,
158 tag: Option<String>,
159 child: AgentSelector,
160 },
161}
162
163fn instance_route(state_dir: &std::path::Path, hierarchy: String) -> Route {
168 let (dir, key) = super::locks::agent_instance_lock(state_dir, &hierarchy);
169 let child = AgentSelector::Instance {
170 parent_agent_instance_hierarchy: Some(
171 crate::db::tags::parent_of(&hierarchy).to_string(),
172 ),
173 agent_instance: crate::db::tags::leaf_of(&hierarchy).to_string(),
174 };
175 Route::Locked {
176 dir,
177 key,
178 hierarchy: Some(hierarchy),
179 tag: None,
180 child,
181 }
182}
183
184async fn spawn_locked(
193 agent: AgentSelector,
194 content: RichContent,
195 seed: Option<i64>,
196 claim: LockClaim,
197) -> Result<Response, Error> {
198 spawn_child(agent, content, seed, Some(claim)).await
199}
200
201async fn spawn_child(
210 agent: AgentSelector,
211 content: RichContent,
212 seed: Option<i64>,
213 transfer: Option<LockClaim>,
214) -> Result<Response, Error> {
215 let child_request = spawn_sdk::Request {
216 path_type: spawn_sdk::Path::AgentsSpawn,
217 message: RequestMessage::Inline(content),
218 agent,
219 dangerous_advanced: Some(spawn_sdk::RequestDangerousAdvanced {
220 stream: Some(true),
221 seed,
222 }),
223 base: Default::default(),
224 };
225
226 let exe = std::env::current_exe()
227 .map_err(|e| Error::Spawn("current_exe".into(), e))?;
228 let mut executor = BinaryExecutor::from_path(exe).detach(true);
229 if let Some(claim) = transfer {
230 executor = executor.transfer_lock(claim);
231 }
232 let mut stream = executor
233 .execute::<spawn_sdk::Request, spawn_sdk::ResponseItem>(child_request, None)
234 .await
235 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
236 let first = stream
237 .next()
238 .await
239 .ok_or(Error::EmptyStream)?
240 .map_err(|e| Error::Instance(format!("exec agents spawn child: {e}")))?;
241 match first {
242 spawn_sdk::ResponseItem::Id(agent_instance_hierarchy) => Ok(Response::Id {
243 agent_instance_hierarchy,
244 }),
245 spawn_sdk::ResponseItem::Chunk(_) => Err(Error::Instance(
246 "agents spawn child emitted a chunk before its id".to_string(),
247 )),
248 }
249}
250
251pub async fn resolve_message(
252 ctx: &Context,
253 message: RequestMessage,
254) -> Result<RichContent, Error> {
255 let (simple, inline, file, python_inline, python_file) = match message {
256 RequestMessage::Inline(rich) => return Ok(rich),
257 RequestMessage::Simple(s) => (Some(s), None, None, None, None),
258 RequestMessage::File(p) => (None, None, Some(p), None, None),
259 RequestMessage::PythonInline(code) => (None, None, None, Some(code), None),
260 RequestMessage::PythonFile(p) => (None, None, None, None, Some(p)),
261 };
262 crate::source_resolver::resolve_source(
263 ctx,
264 simple,
265 inline,
266 file,
267 python_inline,
268 python_file,
269 RichContent::Text,
270 )
271 .await
272}
273
274pub mod request_schema {
275 use objectiveai_sdk::cli::command::agents::message as sdk;
276 use objectiveai_sdk::cli::command::agents::message::request_schema::{Request, Response};
277
278 use crate::context::Context;
279 use crate::error::Error;
280
281 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
282 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
283 }
284}
285
286pub mod response_schema {
287 use objectiveai_sdk::cli::command::agents::message as sdk;
288 use objectiveai_sdk::cli::command::agents::message::response_schema::{Request, Response};
289
290 use crate::context::Context;
291 use crate::error::Error;
292
293 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
294 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
295 }
296}