objectiveai_cli/command/agents/spawn.rs
1//! `agents spawn` — in-process chunk-or-id streaming handler.
2//!
3//! The agent input is the shared [`AgentSelector`] — a direct ref
4//! (inline / file / python / remote), a tag, or an existing
5//! instance. Tags resolve first: BOUND → the live hierarchy
6//! (historic case), GROUPED → the group's stored spec plus the tag
7//! threaded into the conduit for the BOUND upgrade, ABSENT → error.
8//!
9//! Stream-true (`dangerous_advanced.stream = Some(true)`): resolve
10//! + lock + drive the SDK streaming WS connection inside this cli
11//! process. The INITIAL lock (try_acquire, failure = error): historic
12//! case → the AIH lock, un-upgraded tag case → the tag lock, plain ref
13//! → no initial lock. When a parent `agents message` transferred a
14//! claim into this process, the lockfile adopts it lazily on this first
15//! `try_acquire`, so the acquisition succeeds instantly. Historic spawns
16//! load their agent params +
17//! continuation from the stored session. Mid-stream, every newly
18//! revealed hierarchy gets a best-effort AIH claim
19//! ([`AgentInstanceRegistry::observe`]); the first success releases
20//! the tag claim. End-of-stream: if the hierarchy has undelivered
21//! `message_queue` rows, restart with the latest continuation —
22//! restart passes flow into the same output stream.
23//!
24//! Stream-false (the default): re-invoke `objectiveai-cli agents
25//! spawn ...` as a **detached subprocess** with the same arguments
26//! plus `stream = true` (so the resolution + locking above runs in
27//! the child), read the first `ResponseItem::Id` line off the
28//! child's stdout, yield it, and return. The subprocess runs
29//! orphaned to completion (Unix: kernel re-parents to init;
30//! Windows: `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` keeps
31//! it alive past parent exit).
32//!
33//! `params.stream` on the wire is always `Some(true)`; the
34//! `dangerous_advanced.stream` setting only controls cli-side
35//! output.
36
37use std::pin::Pin;
38
39use futures::Stream;
40use futures::StreamExt;
41use objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
42use objectiveai_sdk::agent::completions::message::{Message, UserMessage};
43use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
44use objectiveai_sdk::cli::command::agents::selector::{AgentRef, AgentSelector};
45use objectiveai_sdk::cli::command::agents::spawn::{
46 Request, RequestDangerousAdvanced, ResponseItem,
47};
48use objectiveai_sdk::cli::command::{BinaryExecutor, CommandExecutor};
49
50use crate::context::Context;
51use crate::error::Error;
52use crate::websockets::agent_hierarchies::ChunkAgentHierarchies;
53use crate::websockets::agent_registry::AgentInstanceRegistry;
54
55type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
56
57pub async fn execute(
58 ctx: &Context,
59 request: Request,
60) -> Result<ItemStream, Error> {
61 let want_stream = request
62 .dangerous_advanced
63 .as_ref()
64 .and_then(|a| a.stream)
65 .unwrap_or(false);
66 if want_stream {
67 execute_streaming(ctx, request).await
68 } else {
69 execute_detached(request).await
70 }
71}
72
73/// Stream-false: re-invoke `objectiveai-cli agents spawn`
74/// as a detached subprocess with `stream = true`, capture the
75/// first `ResponseItem::Id` off the child's stdout, yield it, and
76/// return. The subprocess outlives this call — its
77/// `tokio::process::Child` handle is dropped without kill (the
78/// SDK's `BinaryExecutor` default + Windows `DETACHED_PROCESS`
79/// flag).
80async fn execute_detached(request: Request) -> Result<ItemStream, Error> {
81 // Re-invoke with stream=true so the child runs the real
82 // streaming path. Same argv otherwise — `BinaryExecutor` will
83 // ask `Request::into_command()` for it.
84 let mut child_request = request;
85 match child_request.dangerous_advanced.as_mut() {
86 Some(adv) => adv.stream = Some(true),
87 None => {
88 child_request.dangerous_advanced = Some(RequestDangerousAdvanced {
89 stream: Some(true),
90 ..Default::default()
91 })
92 }
93 }
94 // The child is a re-exec of this CLI — it must not inherit the
95 // parent's transform / token budget (timeout survives).
96 crate::command::reexec::strip_inherited(&mut child_request.base);
97
98 // Self-respawn: point the executor at *this* binary (whichever
99 // path the OS recorded for the current process), then arm
100 // Windows-detach so the child survives parent exit. Unix gets
101 // re-parent-to-init for free via the default kill_on_drop=false.
102 let exe = std::env::current_exe()
103 .map_err(|e| Error::Spawn("current_exe".into(), e))?;
104 let executor = BinaryExecutor::from_path(exe).detach(true);
105
106 let mut stream = executor
107 .execute::<Request, ResponseItem>(child_request, None)
108 .await
109 .map_err(|e| Error::Instance(format!(
110 "self-respawn for agents spawn: {e}"
111 )))?;
112
113 // Take exactly the first ResponseItem (the LogStreamReady Id),
114 // yield it, return. Drop the rest of the stream + the Child
115 // handle without kill. On Windows the detach flags keep the
116 // child running; on Unix the kernel re-parents to init.
117 let first = stream
118 .next()
119 .await
120 .ok_or(Error::EmptyStream)?
121 .map_err(|e| Error::Instance(format!(
122 "self-respawn for agents spawn: {e}"
123 )))?;
124 Ok(Box::pin(
125 objectiveai_sdk::cli::command::StreamOnce::new(Ok(first)),
126 ))
127}
128
129/// Spawn modes after selector resolution: a fresh agent (direct
130/// ref, or a GROUPED tag carrying the tag name for the conduit
131/// upgrade) or an existing hierarchy resumed via its stored
132/// session + continuation.
133enum Mode {
134 Fresh {
135 agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional,
136 tag: Option<String>,
137 },
138 Historic {
139 hierarchy: String,
140 },
141}
142
143async fn execute_streaming(
144 ctx: &Context,
145 request: Request,
146) -> Result<ItemStream, Error> {
147 // Required user-message slot — gets wrapped into a single
148 // `Message::User` at the head of the API call's `messages`
149 // array. Reuses `agents message`'s `resolve_message`
150 // so the five wire variants (`Simple` / `Inline(RichContent)`
151 // / `File` / `PythonInline` / `PythonFile`) round-trip
152 // identically. EMPTY resolved content (`--simple ""`, an empty
153 // Inline text, empty parts) means a wake-up/resume turn: send an
154 // EMPTY `messages` array — never a user message with an empty
155 // string — and let the API drive from the continuation + the
156 // conduit's queue drain.
157 let content = super::message::resolve_message(ctx, request.message).await?;
158 let messages = if content.is_empty() {
159 Vec::new()
160 } else {
161 vec![Message::User(UserMessage {
162 content,
163 })]
164 };
165 let seed = request.dangerous_advanced.as_ref().and_then(|a| a.seed);
166
167 // Resolve the agent target AND which laboratory targets its attachments
168 // are keyed on. Labs only apply in Tag/Instance mode (a direct Ref has no
169 // tag/AIH to key on). `run_multi_pass` does the actual DB resolution; here
170 // we only name the targets — the same 3 permutations as before: a grouped
171 // tag → the tag's labs; a resolved (Bound) tag → the tag's UNION the bound
172 // AIH's; an instance → the AIH's.
173 use crate::command::agents::locks::Family;
174 use crate::db::laboratory_attachments::Target;
175 let (mode, lab_targets, family): (Mode, Vec<Target>, Option<Family>) = match request.agent {
176 AgentSelector::Ref { agent } => (
177 Mode::Fresh {
178 agent: resolve_agent_ref(ctx, agent).await?,
179 tag: None,
180 },
181 Vec::new(),
182 None,
183 ),
184 AgentSelector::Tag { agent_tag } => {
185 match crate::db::tags::lookup(ctx.db_client().await?, &agent_tag).await? {
186 crate::db::tags::LookupState::Bound { agent_instance_hierarchy } => {
187 let lab_targets = vec![
188 Target::Tag(agent_tag.clone()),
189 Target::Aih(agent_instance_hierarchy.clone()),
190 ];
191 (
192 Mode::Historic {
193 hierarchy: agent_instance_hierarchy.clone(),
194 },
195 lab_targets,
196 Some(Family::Hierarchy(agent_instance_hierarchy)),
197 )
198 }
199 crate::db::tags::LookupState::Grouped { agent_spec, tag_group_id, .. } => {
200 let lab_targets = vec![Target::Tag(agent_tag.clone())];
201 (
202 Mode::Fresh {
203 agent: agent_spec,
204 tag: Some(agent_tag),
205 },
206 lab_targets,
207 Some(Family::Group(tag_group_id)),
208 )
209 }
210 crate::db::tags::LookupState::Absent => {
211 return Err(Error::TagNotFound(agent_tag));
212 }
213 }
214 }
215 AgentSelector::Instance {
216 parent_agent_instance_hierarchy,
217 agent_instance,
218 } => {
219 let parent = parent_agent_instance_hierarchy
220 .as_deref()
221 .unwrap_or(&ctx.config.agent_instance_hierarchy);
222 let hierarchy = format!("{parent}/{agent_instance}");
223 let lab_targets = vec![Target::Aih(hierarchy.clone())];
224 (
225 Mode::Historic {
226 hierarchy: hierarchy.clone(),
227 },
228 lab_targets,
229 Some(Family::Hierarchy(hierarchy)),
230 )
231 }
232 };
233
234 // Initial lock + params assembly. Acquire the agent's whole lock FAMILY
235 // (all-or-nothing, NON-BLOCKING) so that while it's live none of its tags
236 // can be relocated (`tags apply`) or have labs detached (`laboratories
237 // detach`): a GROUPED tag locks every tag in its group; a bound tag / AIH
238 // locks the AIH plus every tag bound to it. A held member means the agent
239 // (or another spawn of the tag) is already live → error. When the parent
240 // `agents message` transferred the family into this process, the lockfile
241 // adopts each claim lazily on this first acquire, so they re-acquire
242 // INSTANTLY rather than conflicting with the inherited handles. Mid-stream
243 // best-effort AIH claims in `run_multi_pass` are unaffected.
244 let state_dir = ctx.filesystem.state_dir();
245 let mut registry = AgentInstanceRegistry::new(state_dir.clone(), ctx.agent_locks_arc());
246 if let Some(family) = family {
247 let is_group = matches!(family, Family::Group(_));
248 match super::locks::try_acquire_family(
249 ctx.agent_locks(),
250 ctx.db_client().await?,
251 &state_dir,
252 family,
253 )
254 .await?
255 {
256 Some(fam) => {
257 if let Some((hierarchy, aih_lock)) = fam.aih {
258 registry.preseed(hierarchy, aih_lock);
259 }
260 registry.hold_tag_claims(fam.tags);
261 }
262 None if is_group => {
263 // GROUPED: name the requested tag in the error.
264 let tag = match &mode {
265 Mode::Fresh { tag: Some(tag), .. } => tag.clone(),
266 _ => String::new(),
267 };
268 return Err(Error::AgentTagActive { tag });
269 }
270 None => {
271 let agent_instance_hierarchy = match &mode {
272 Mode::Historic { hierarchy } => hierarchy.clone(),
273 _ => String::new(),
274 };
275 return Err(Error::AgentInstanceActive {
276 agent_instance_hierarchy,
277 });
278 }
279 }
280 }
281 let (agent, agent_tag, continuation) = match mode {
282 Mode::Fresh { agent, tag } => (agent, tag, None),
283 Mode::Historic { hierarchy } => {
284 let lookup = crate::db::logs::lookup_session(ctx.db_client().await?, &hierarchy)
285 .await?
286 .ok_or(Error::AgentNoPriorRequest {
287 agent_instance_hierarchy: hierarchy,
288 })?;
289 (lookup.agent, None, lookup.continuation)
290 }
291 };
292
293 // Message-queue delivery to the live API happens through the
294 // conduit's `read_pending_and_upgrade_tag` call — the API
295 // pulls pending rows on demand as the stream runs and stamps
296 // their ids onto the first emitted assistant chunk's
297 // `request_message_ids`. No pre-spawn drain + prepend here.
298 //
299 // `run_multi_pass` builds the create-params and resolves the laboratory
300 // attachments (from `lab_targets`) internally.
301 let ctx_clone = ctx.clone();
302 Ok(Box::pin(run_multi_pass(
303 ctx_clone,
304 messages,
305 agent,
306 seed,
307 continuation,
308 agent_tag,
309 lab_targets,
310 registry,
311 )))
312}
313
314/// Drives one or more stream passes until no seen hierarchy has
315/// pending `message_queue` items. Each pass opens a fresh WS
316/// stream + log writer + MCP server + conduit; the
317/// [`AgentInstanceRegistry`] (carrying any initial AIH/tag claim)
318/// persists across passes so an agent's lock stays held for the
319/// whole spawn lifetime, not per-pass — and is released when the
320/// stream (and with it the registry) drops.
321/// Resolve the laboratory ids attached to `lab_targets` into the request's
322/// `laboratories` value. Lists every target CONCURRENTLY (one pool serves
323/// concurrent queries), then flattens + dedups (first-seen order). `None` when
324/// no targets / no attachments. Shared by `agents spawn` and `agents queue
325/// deliver` (both go through `run_multi_pass`). No liveness check — the conduit
326/// dials each laboratory on demand at MCP-initialize time.
327async fn resolve_laboratories(
328 ctx: &Context,
329 lab_targets: &[crate::db::laboratory_attachments::Target],
330) -> Result<Option<Vec<objectiveai_sdk::laboratories::Laboratory>>, Error> {
331 if lab_targets.is_empty() {
332 return Ok(None);
333 }
334 let pool = ctx.db_client().await?;
335 let lists = futures::future::try_join_all(
336 lab_targets
337 .iter()
338 .map(|target| crate::db::laboratory_attachments::list(pool, target)),
339 )
340 .await?;
341 let mut ids: Vec<String> = Vec::new();
342 for list in lists {
343 for id in list {
344 if !ids.contains(&id) {
345 ids.push(id);
346 }
347 }
348 }
349 if ids.is_empty() {
350 return Ok(None);
351 }
352 Ok(Some(
353 ids.into_iter()
354 .map(|id| {
355 objectiveai_sdk::laboratories::Laboratory::Client(
356 objectiveai_sdk::laboratories::ClientLaboratory {
357 r#type: objectiveai_sdk::laboratories::ClientLaboratoryType::Client,
358 id,
359 },
360 )
361 })
362 .collect(),
363 ))
364}
365
366#[allow(clippy::too_many_arguments)]
367pub(crate) fn run_multi_pass(
368 ctx: Context,
369 messages: Vec<Message>,
370 agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional,
371 seed: Option<i64>,
372 continuation: Option<String>,
373 agent_tag: Option<String>,
374 lab_targets: Vec<crate::db::laboratory_attachments::Target>,
375 mut registry: AgentInstanceRegistry,
376) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
377 async_stream::try_stream! {
378 // Resolve the agent's laboratory attachments (from the named targets)
379 // and assemble the create-params. `provider`/`response_format` are
380 // always defaulted and `stream` is always true for the in-process WS
381 // path; only `messages`/`continuation` change across restart passes.
382 let laboratories = resolve_laboratories(&ctx, &lab_targets).await?;
383 let mut params = AgentCompletionCreateParams {
384 messages,
385 provider: None,
386 agent,
387 response_format: None,
388 seed,
389 stream: Some(true),
390 continuation,
391 laboratories,
392 };
393 // A spawn has exactly one `(agent_instance_hierarchy,
394 // agent_full_id)` pair — set by the API on the very first
395 // chunk and never changes across restart passes. Capture
396 // once; reuse forever. `None` until the first chunk lands.
397 let mut identity: Option<(String, String)> = None;
398 // Has `ResponseItem::Id` been yielded yet? Persists across
399 // restart passes — the spawn-id handshake is a one-time
400 // event, gated on the LogWriter's `written_once` signal so
401 // the caller only sees the Id after at least one log row
402 // has been persisted.
403 let mut id_emitted = false;
404 // Resolve the MCP client tuning once for the whole spawn; every
405 // pass's conduit reuses these (cheap to pass per pass).
406 let mcp_timeout_ms = ctx.resolve_mcp_timeout_ms().await?;
407 let backoff_max_elapsed_time_ms =
408 ctx.resolve_backoff_max_elapsed_time_ms().await?;
409
410 loop {
411 // Per-pass resources. New WS connection, new log writer,
412 // new conduit + MCP server. The registry survives across
413 // passes (see above).
414 let mcp_server =
415 crate::websockets::mcp_server::spawn(ctx.clone());
416 let conduit =
417 crate::websockets::conduit::ConduitMcpHandler::new(
418 mcp_server,
419 ctx.clone(),
420 agent_tag.clone(),
421 mcp_timeout_ms,
422 backoff_max_elapsed_time_ms,
423 );
424 // Spawn.rs doesn't need the primary-id ready signal —
425 // it yields `ResponseItem::Id` from
426 // `chunk.agent_instance_hierarchy` directly on the first
427 // chunk. Drop the receiver.
428 let (log_writer, _ready_rx) = crate::db::logs::write_agent_completion(
429 ctx.db_client().await?,
430 ¶ms,
431 ctx.config.agent_instance_hierarchy.clone(),
432 )
433 .map_err(|e| Error::Instance(format!(
434 "failed to build agent-completion log writer: {e}"
435 )))?;
436
437 let (sdk_stream, notifier) =
438 objectiveai_sdk::agent::completions::create_agent_completion_streaming(
439 ctx.api_client().await?,
440 params.clone(),
441 conduit.clone(),
442 )
443 .await
444 .map_err(|e| Error::Instance(format!(
445 "failed to open agent-completion stream: {e}"
446 )))?;
447 conduit.install_notifier(notifier);
448
449 let mut sdk_stream = Box::pin(sdk_stream);
450 let mut last_continuation: Option<String> = None;
451 // Per-pass buffer of chunks held back until the
452 // LogWriter confirms it has persisted at least once.
453 // Only meaningful for pass 1 — pass 2+ already has
454 // `id_emitted = true` from a prior pass, so the buffer
455 // gate never triggers and chunks flow through directly.
456 let mut buffered: Vec<
457 objectiveai_sdk::agent::completions::response::streaming::AgentCompletionChunk,
458 > = Vec::new();
459 let mut stream_err: Option<String> = None;
460
461 while let Some(item) = sdk_stream.next().await {
462 let chunk = match item {
463 Ok(c) => c,
464 Err(e) => {
465 stream_err = Some(format!("agent stream item error: {e}"));
466 break;
467 }
468 };
469
470 // First chunk EVER (first pass, first chunk):
471 // capture the spawn's identity + claim the lock
472 // file. Tag-group upgrade is owned by the conduit's
473 // `read_pending_and_upgrade_tag`, which the API
474 // fires before the very first chunk is produced —
475 // no upgrade fan-out is needed here. The
476 // `ResponseItem::Id` handshake itself fires later,
477 // gated on `log_writer.written_once()`.
478 if identity.is_none() {
479 let hier = chunk.agent_instance_hierarchy.clone();
480 let full_id = chunk.agent_full_id.clone();
481 registry.observe(&hier).await;
482 identity = Some((hier, full_id));
483 }
484
485 // Latest continuation seen on the wire — what we
486 // use to restart if pending messages turn up at
487 // EOF. Only the terminal chunk usually carries one.
488 if let Some(c) = chunk.continuation.as_deref() {
489 last_continuation = Some(c.to_string());
490 }
491
492 // Upsert any `(AIH, continuation)` pairs the chunk
493 // carries into the `agent_continuations` registry
494 // (cumulative chunks always yield exactly one pair;
495 // the Vec is 0-or-1 long depending on whether
496 // `continuation` is `Some`). Awaited before the
497 // log-writer send + downstream yield so the registry
498 // row is visible by the time the chunk leaves this
499 // body.
500 let mut continuation_upserts: Vec<_> = Vec::new();
501 for (hier, continuation) in chunk.agent_instance_hierarchies() {
502 if let Some(c) = continuation {
503 continuation_upserts.push(
504 crate::db::agent_continuations::upsert(ctx.db_client().await?, hier, c),
505 );
506 }
507 }
508 if let Err(e) =
509 futures::future::try_join_all(continuation_upserts).await
510 {
511 stream_err =
512 Some(format!("agent_continuations upsert: {e}"));
513 break;
514 }
515
516 // Log + forward. The write is a synchronous mpsc
517 // send into the LogWriter's listener task — DB IO
518 // happens off this critical path. Clone the chunk
519 // for the listener; the original yields downstream
520 // (or sits in the buffer until the Id gate opens).
521 if let Err(e) = log_writer.write(chunk.clone()) {
522 stream_err = Some(format!("log writer error: {e}"));
523 break;
524 }
525
526 // Id gate: once the LogWriter signals it has
527 // persisted at least one batch, yield the Id and
528 // drain any chunks buffered up to this point. The
529 // gate flips exactly once per spawn (across all
530 // passes) — `id_emitted` persists outside the
531 // restart loop.
532 if !id_emitted && log_writer.written_once() {
533 let (hier, _) = identity
534 .as_ref()
535 .expect("identity set above on the first chunk");
536 yield ResponseItem::Id(hier.clone());
537 for c in buffered.drain(..) {
538 yield ResponseItem::Chunk(c);
539 }
540 id_emitted = true;
541 }
542
543 if id_emitted {
544 yield ResponseItem::Chunk(chunk);
545 } else {
546 buffered.push(chunk);
547 }
548 }
549
550 // Post-stream: if the SDK closed before the LogWriter
551 // ever flipped `written_once` true (e.g. very fast EOF
552 // ahead of the listener's first batch), wait for the
553 // first persistence to land, then emit the Id + drain
554 // any held chunks. Only fires when we actually have
555 // chunks queued behind the gate.
556 if !id_emitted && !buffered.is_empty() {
557 if let Err(e) = log_writer.wait_written_once().await {
558 stream_err.get_or_insert_with(|| format!("log writer wait: {e}"));
559 } else {
560 let (hier, _) = identity
561 .as_ref()
562 .expect("identity set on the first chunk");
563 yield ResponseItem::Id(hier.clone());
564 for c in buffered.drain(..) {
565 yield ResponseItem::Chunk(c);
566 }
567 id_emitted = true;
568 }
569 }
570
571 // Finalize the log writer (consumes it; drops the
572 // sender; awaits the listener task). By construction
573 // this returns only after the queue is empty AND no
574 // work is in flight.
575 if let Err(e) = log_writer.finalize().await {
576 stream_err.get_or_insert_with(|| format!("log writer finalize: {e}"));
577 }
578 drop(sdk_stream);
579 drop(conduit);
580
581 if let Some(e) = stream_err {
582 Err(Error::Instance(e))?;
583 }
584
585 // End-of-pass: a pure EXISTS check against the spawn's
586 // single hierarchy. The conduit already promoted every
587 // sibling tag in the group during its in-stream reads
588 // via `read_pending_and_upgrade_tag` — so this check
589 // sees the post-upgrade `tags` state and catches
590 // anything queued mid-stream against a now-BOUND
591 // sibling. On `false`, fall through to the implicit
592 // registry drop on function return (no explicit destroy
593 // needed — there's only one claim and we're done with it).
594 let Some((hier, _full_id)) = identity.as_ref() else {
595 // Empty stream — nothing was claimed, nothing to
596 // restart. Just exit.
597 break;
598 };
599 let pending = crate::db::message_queue::check_any_pending(
600 ctx.db_client().await?, hier,
601 )
602 .await
603 .unwrap_or(false);
604 if !pending {
605 break;
606 }
607
608 // Restart with the latest continuation only. No new
609 // messages — the API picks up state from the
610 // continuation token. Re-resolve the agent's laboratory
611 // attachments too: one may have been attached or detached
612 // while this pass ran, and each pass must dial whatever is
613 // attached NOW.
614 params.messages = Vec::new();
615 params.continuation = last_continuation;
616 params.laboratories = resolve_laboratories(&ctx, &lab_targets).await?;
617 }
618 }
619}
620
621/// Resolve an [`AgentRef`] into a typed agent. `Resolved` passes
622/// through; `File` / `PythonInline` / `PythonFile` run their IO /
623/// Python here via the shared 5-variant resolver (the `simple`
624/// slot is never populated for agent refs — `--agent <ref>`
625/// strings parse at the clap layer).
626pub(crate) async fn resolve_agent_ref(
627 ctx: &Context,
628 agent: AgentRef,
629) -> Result<InlineAgentBaseWithFallbacksOrRemoteCommitOptional, Error> {
630 let (file, python_inline, python_file) = match agent {
631 AgentRef::Resolved(resolved) => return Ok(resolved),
632 AgentRef::File(p) => (Some(p), None, None),
633 AgentRef::PythonInline(code) => (None, Some(code), None),
634 AgentRef::PythonFile(p) => (None, None, Some(p)),
635 };
636 crate::source_resolver::resolve_source(
637 ctx,
638 None,
639 None,
640 file,
641 python_inline,
642 python_file,
643 |_| unreachable!("agent refs have no plain-text variant"),
644 )
645 .await
646}
647
648pub mod request_schema {
649 use objectiveai_sdk::cli::command::agents::spawn as sdk;
650 use objectiveai_sdk::cli::command::agents::spawn::request_schema::{Request, Response};
651
652 use crate::context::Context;
653 use crate::error::Error;
654
655 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
656 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
657 }
658}
659
660pub mod response_schema {
661 use objectiveai_sdk::cli::command::agents::spawn as sdk;
662 use objectiveai_sdk::cli::command::agents::spawn::response_schema::{Request, Response};
663
664 use crate::context::Context;
665 use crate::error::Error;
666
667 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
668 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
669 }
670}