use std::pin::Pin;
use futures::Stream;
use futures::StreamExt;
use objectiveai_sdk::agent::InlineAgentBaseWithFallbacksOrRemoteCommitOptional;
use objectiveai_sdk::agent::completions::message::{Message, UserMessage};
use objectiveai_sdk::agent::completions::request::AgentCompletionCreateParams;
use objectiveai_sdk::cli::command::agents::selector::{AgentRef, AgentSelector};
use objectiveai_sdk::cli::command::agents::spawn::{
Request, RequestDangerousAdvanced, ResponseItem,
};
use objectiveai_sdk::cli::command::{BinaryExecutor, CommandExecutor};
use crate::context::Context;
use crate::error::Error;
use crate::websockets::agent_hierarchies::ChunkAgentHierarchies;
use crate::websockets::agent_registry::AgentInstanceRegistry;
type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
pub async fn execute(
ctx: &Context,
request: Request,
) -> Result<ItemStream, Error> {
let want_stream = request
.dangerous_advanced
.as_ref()
.and_then(|a| a.stream)
.unwrap_or(false);
if want_stream {
execute_streaming(ctx, request).await
} else {
execute_detached(request).await
}
}
async fn execute_detached(request: Request) -> Result<ItemStream, Error> {
let mut child_request = request;
match child_request.dangerous_advanced.as_mut() {
Some(adv) => adv.stream = Some(true),
None => {
child_request.dangerous_advanced = Some(RequestDangerousAdvanced {
stream: Some(true),
..Default::default()
})
}
}
crate::command::reexec::strip_inherited(&mut child_request.base);
let exe = std::env::current_exe()
.map_err(|e| Error::Spawn("current_exe".into(), e))?;
let executor = BinaryExecutor::from_path(exe).detach(true);
let mut stream = executor
.execute::<Request, ResponseItem>(child_request, None)
.await
.map_err(|e| Error::Instance(format!(
"self-respawn for agents spawn: {e}"
)))?;
let first = stream
.next()
.await
.ok_or(Error::EmptyStream)?
.map_err(|e| Error::Instance(format!(
"self-respawn for agents spawn: {e}"
)))?;
Ok(Box::pin(
objectiveai_sdk::cli::command::StreamOnce::new(Ok(first)),
))
}
enum Mode {
Fresh {
agent: InlineAgentBaseWithFallbacksOrRemoteCommitOptional,
tag: Option<String>,
},
Historic {
hierarchy: String,
},
}
async fn execute_streaming(
ctx: &Context,
request: Request,
) -> Result<ItemStream, Error> {
let content = super::message::resolve_message(ctx, request.message).await?;
let messages = if content.is_empty() {
Vec::new()
} else {
vec![Message::User(UserMessage {
content,
})]
};
let seed = request.dangerous_advanced.as_ref().and_then(|a| a.seed);
let skip_lock = request
.dangerous_advanced
.as_ref()
.and_then(|a| a.skip_lock)
.unwrap_or(false);
let mode = match request.agent {
AgentSelector::Ref { agent } => Mode::Fresh {
agent: resolve_agent_ref(ctx, agent).await?,
tag: None,
},
AgentSelector::Tag { agent_tag } => {
match crate::db::tags::lookup(ctx.db_client().await?, &agent_tag).await? {
crate::db::tags::LookupState::Bound { agent_instance_hierarchy } => {
Mode::Historic {
hierarchy: agent_instance_hierarchy,
}
}
crate::db::tags::LookupState::Grouped { agent_spec, .. } => {
let agent = agent_spec;
Mode::Fresh {
agent,
tag: Some(agent_tag),
}
}
crate::db::tags::LookupState::Absent => {
return Err(Error::TagNotFound(agent_tag));
}
}
}
AgentSelector::Instance {
parent_agent_instance_hierarchy,
agent_instance,
} => {
let parent = parent_agent_instance_hierarchy
.as_deref()
.unwrap_or(&ctx.config.agent_instance_hierarchy);
Mode::Historic {
hierarchy: format!("{parent}/{agent_instance}"),
}
}
};
let state_dir = ctx.filesystem.state_dir();
let mut registry = AgentInstanceRegistry::new(state_dir.clone());
let (agent, agent_tag, continuation) = match mode {
Mode::Fresh { agent, tag } => {
if !skip_lock {
if let Some(tag) = &tag {
let (dir, key) = super::locks::agent_tag_lock(&state_dir, tag);
match objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await {
Some(claim) => registry.hold_tag_claim(claim),
None => return Err(Error::AgentTagActive { tag: tag.clone() }),
}
}
}
(agent, tag, None)
}
Mode::Historic { hierarchy } => {
if !skip_lock {
let (dir, key) = super::locks::agent_instance_lock(&state_dir, &hierarchy);
match objectiveai_sdk::lockfile::try_acquire(&dir, &key, "").await {
Some(claim) => registry.preseed(hierarchy.clone(), claim),
None => {
return Err(Error::AgentInstanceActive {
agent_instance_hierarchy: hierarchy,
});
}
}
}
let lookup = crate::db::logs::lookup_session(ctx.db_client().await?, &hierarchy)
.await?
.ok_or(Error::AgentNoPriorRequest {
agent_instance_hierarchy: hierarchy,
})?;
(lookup.agent, None, lookup.continuation)
}
};
let params = AgentCompletionCreateParams {
messages,
provider: None,
agent,
response_format: None,
seed,
stream: Some(true),
continuation,
};
let ctx_clone = ctx.clone();
Ok(Box::pin(run_multi_pass(ctx_clone, params, agent_tag, registry)))
}
pub(crate) fn run_multi_pass(
ctx: Context,
initial_params: AgentCompletionCreateParams,
agent_tag: Option<String>,
mut registry: AgentInstanceRegistry,
) -> impl Stream<Item = Result<ResponseItem, Error>> + Send {
async_stream::try_stream! {
let mut params = initial_params;
let mut identity: Option<(String, String)> = None;
let mut id_emitted = false;
let mcp_timeout_ms = ctx.resolve_mcp_timeout_ms().await?;
let backoff_max_elapsed_time_ms =
ctx.resolve_backoff_max_elapsed_time_ms().await?;
loop {
let mcp_server =
crate::websockets::mcp_server::spawn(ctx.clone());
let conduit =
crate::websockets::conduit::ConduitMcpHandler::new(
mcp_server,
ctx.clone(),
agent_tag.clone(),
mcp_timeout_ms,
backoff_max_elapsed_time_ms,
);
let (log_writer, _ready_rx) = crate::db::logs::write_agent_completion(
ctx.db_client().await?,
¶ms,
ctx.config.agent_instance_hierarchy.clone(),
)
.map_err(|e| Error::Instance(format!(
"failed to build agent-completion log writer: {e}"
)))?;
let (sdk_stream, notifier) =
objectiveai_sdk::agent::completions::create_agent_completion_streaming(
ctx.api_client().await?,
params.clone(),
conduit.clone(),
)
.await
.map_err(|e| Error::Instance(format!(
"failed to open agent-completion stream: {e}"
)))?;
conduit.install_notifier(notifier);
let mut sdk_stream = Box::pin(sdk_stream);
let mut last_continuation: Option<String> = None;
let mut buffered: Vec<
objectiveai_sdk::agent::completions::response::streaming::AgentCompletionChunk,
> = Vec::new();
let mut stream_err: Option<String> = None;
while let Some(item) = sdk_stream.next().await {
let chunk = match item {
Ok(c) => c,
Err(e) => {
stream_err = Some(format!("agent stream item error: {e}"));
break;
}
};
if identity.is_none() {
let hier = chunk.agent_instance_hierarchy.clone();
let full_id = chunk.agent_full_id.clone();
registry.observe(&hier).await;
identity = Some((hier, full_id));
}
if let Some(c) = chunk.continuation.as_deref() {
last_continuation = Some(c.to_string());
}
let mut continuation_upserts: Vec<_> = Vec::new();
for (hier, continuation) in chunk.agent_instance_hierarchies() {
if let Some(c) = continuation {
continuation_upserts.push(
crate::db::agent_continuations::upsert(ctx.db_client().await?, hier, c),
);
}
}
if let Err(e) =
futures::future::try_join_all(continuation_upserts).await
{
stream_err =
Some(format!("agent_continuations upsert: {e}"));
break;
}
if let Err(e) = log_writer.write(chunk.clone()) {
stream_err = Some(format!("log writer error: {e}"));
break;
}
if !id_emitted && log_writer.written_once() {
let (hier, _) = identity
.as_ref()
.expect("identity set above on the first chunk");
yield ResponseItem::Id(hier.clone());
for c in buffered.drain(..) {
yield ResponseItem::Chunk(c);
}
id_emitted = true;
}
if id_emitted {
yield ResponseItem::Chunk(chunk);
} else {
buffered.push(chunk);
}
}
if !id_emitted && !buffered.is_empty() {
if let Err(e) = log_writer.wait_written_once().await {
stream_err.get_or_insert_with(|| format!("log writer wait: {e}"));
} else {
let (hier, _) = identity
.as_ref()
.expect("identity set on the first chunk");
yield ResponseItem::Id(hier.clone());
for c in buffered.drain(..) {
yield ResponseItem::Chunk(c);
}
id_emitted = true;
}
}
if let Err(e) = log_writer.finalize().await {
stream_err.get_or_insert_with(|| format!("log writer finalize: {e}"));
}
drop(sdk_stream);
drop(conduit);
if let Some(e) = stream_err {
Err(Error::Instance(e))?;
}
let Some((hier, _full_id)) = identity.as_ref() else {
break;
};
let pending = crate::db::message_queue::check_any_pending(
ctx.db_client().await?, hier,
)
.await
.unwrap_or(false);
if !pending {
break;
}
params.messages = Vec::new();
params.continuation = last_continuation;
}
}
}
pub(crate) async fn resolve_agent_ref(
ctx: &Context,
agent: AgentRef,
) -> Result<InlineAgentBaseWithFallbacksOrRemoteCommitOptional, Error> {
let (file, python_inline, python_file) = match agent {
AgentRef::Resolved(resolved) => return Ok(resolved),
AgentRef::File(p) => (Some(p), None, None),
AgentRef::PythonInline(code) => (None, Some(code), None),
AgentRef::PythonFile(p) => (None, None, Some(p)),
};
crate::source_resolver::resolve_source(
ctx,
None,
None,
file,
python_inline,
python_file,
|_| unreachable!("agent refs have no plain-text variant"),
)
.await
}
pub mod request_schema {
use objectiveai_sdk::cli::command::agents::spawn as sdk;
use objectiveai_sdk::cli::command::agents::spawn::request_schema::{Request, Response};
use crate::context::Context;
use crate::error::Error;
pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
}
}
pub mod response_schema {
use objectiveai_sdk::cli::command::agents::spawn as sdk;
use objectiveai_sdk::cli::command::agents::spawn::response_schema::{Request, Response};
use crate::context::Context;
use crate::error::Error;
pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
}
}