use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use zagens_core::engine::hosts::SubAgentHost;
use zagens_core::engine::{SubAgentSpawnError, SubAgentSpawnOutcome};
use zagens_core::subagent::SubAgentResult;
use crate::agent_surface::AppMode;
use super::Engine;
const SUBAGENT_LIST_CLEANUP_MAX_AGE: Duration = Duration::from_secs(60 * 60);
#[async_trait]
impl SubAgentHost for Engine {
async fn spawn_general(
&self,
prompt: &str,
) -> Result<SubAgentSpawnOutcome, SubAgentSpawnError> {
Engine::spawn_general_subagent(self, prompt).await
}
async fn list_with_cleanup(&self) -> Vec<SubAgentResult> {
Engine::list_subagents(self).await
}
async fn running_count(&self) -> usize {
let mut mgr = self.runtime_ext().subagent_manager.write().await;
mgr.running_count()
}
}
impl Engine {
pub(in crate::core::engine) async fn spawn_general_subagent(
&self,
prompt: &str,
) -> Result<SubAgentSpawnOutcome, SubAgentSpawnError> {
use crate::tools::subagent::{
SubAgentRuntime, SubAgentType, resolve_subagent_assignment_route,
};
let Some(client) = self.deepseek_client.clone() else {
let message = self
.deepseek_client_error
.as_deref()
.map(|err| format!("Failed to spawn sub-agent: {err}"))
.unwrap_or_else(|| {
"Failed to spawn sub-agent: API client not configured".to_string()
});
return Err(if self.deepseek_client_error.is_some() {
SubAgentSpawnError::SpawnFailed(message)
} else {
SubAgentSpawnError::NoClient
});
};
let mut runtime = SubAgentRuntime::new(
client,
self.session.model.clone(),
self.build_tool_context(AppMode::Agent, self.session.auto_approve),
self.session.allow_shell,
Some(self.tx_event.clone()),
Arc::clone(&self.runtime_ext().subagent_manager),
)
.with_role_models(self.config.subagent_model_overrides.clone())
.with_auto_model(self.session.auto_model)
.with_reasoning_effort(
self.session.reasoning_effort.clone(),
self.session.reasoning_effort_auto,
)
.with_max_spawn_depth(self.config.max_spawn_depth)
.with_step_timeout(self.config.subagent_step_timeout)
.background_runtime();
let route = resolve_subagent_assignment_route(&runtime, None, prompt).await;
runtime.model = route.model;
runtime.reasoning_effort = route.reasoning_effort;
runtime.reasoning_effort_auto = false;
let snapshot = {
let mut manager = self.runtime_ext().subagent_manager.write().await;
manager
.spawn_background(
Arc::clone(&self.runtime_ext().subagent_manager),
runtime,
SubAgentType::General,
prompt.to_string(),
None,
)
.map_err(|err| SubAgentSpawnError::SpawnFailed(err.to_string()))?
};
Ok(SubAgentSpawnOutcome {
agent_id: snapshot.agent_id,
})
}
pub(in crate::core::engine) async fn list_subagents(&self) -> Vec<SubAgentResult> {
let mut manager = self.runtime_ext().subagent_manager.write().await;
manager.cleanup(SUBAGENT_LIST_CLEANUP_MAX_AGE);
manager.list()
}
pub(in crate::core::engine) async fn spawn_macro_craft_review(
&self,
task_id: &str,
prompt: &str,
) -> Result<SubAgentSpawnOutcome, SubAgentSpawnError> {
use crate::tools::subagent::{SubAgentRuntime, SubAgentSpawnOptions, SubAgentType};
use zagens_core::subagent::SubAgentAssignment;
let Some(client) = self.deepseek_client.clone() else {
let message = self
.deepseek_client_error
.as_deref()
.map(|err| format!("Failed to spawn CRAFT review: {err}"))
.unwrap_or_else(|| {
"Failed to spawn CRAFT review: API client not configured".to_string()
});
return Err(if self.deepseek_client_error.is_some() {
SubAgentSpawnError::SpawnFailed(message)
} else {
SubAgentSpawnError::NoClient
});
};
let runtime = SubAgentRuntime::new(
client,
self.session.model.clone(),
self.build_tool_context(AppMode::Agent, self.session.auto_approve),
self.session.allow_shell,
Some(self.tx_event.clone()),
Arc::clone(&self.runtime_ext().subagent_manager),
)
.with_role_models(self.config.subagent_model_overrides.clone())
.with_auto_model(self.session.auto_model)
.with_reasoning_effort(
self.session.reasoning_effort.clone(),
self.session.reasoning_effort_auto,
)
.with_max_spawn_depth(self.config.max_spawn_depth)
.with_step_timeout(self.config.subagent_step_timeout)
.background_runtime();
let assignment = SubAgentAssignment::new(prompt.to_string(), None);
let options = SubAgentSpawnOptions {
task_id: Some(task_id.to_string()),
nickname: Some("LHT review".to_string()),
..SubAgentSpawnOptions::default()
};
let snapshot = {
let mut manager = self.runtime_ext().subagent_manager.write().await;
manager
.spawn_background_with_assignment_options(
Arc::clone(&self.runtime_ext().subagent_manager),
runtime,
SubAgentType::Review,
prompt.to_string(),
assignment,
None,
options,
)
.map_err(|err| SubAgentSpawnError::SpawnFailed(err.to_string()))?
};
Ok(SubAgentSpawnOutcome {
agent_id: snapshot.agent_id,
})
}
pub(in crate::core::engine) async fn handle_spawn_subagent_op(&self, prompt: &str) {
use crate::core::events::Event;
use zagens_core::error_taxonomy::ErrorEnvelope;
match self.spawn_general_subagent(prompt).await {
Ok(outcome) => {
let _ = self
.tx_event
.send(Event::status(format!(
"Spawned sub-agent {}",
outcome.agent_id
)))
.await;
}
Err(SubAgentSpawnError::NoClient) => {
let _ = self
.tx_event
.send(Event::error(ErrorEnvelope::fatal(
"Failed to spawn sub-agent: API client not configured",
)))
.await;
}
Err(SubAgentSpawnError::SpawnFailed(message)) => {
let _ = self
.tx_event
.send(Event::error(ErrorEnvelope::fatal(message)))
.await;
}
}
}
}