use crate::config::ProjectConfig; use crate::http::context::{AppContext, OpenApiResult, bad_request}; use poem_openapi::{Object, OpenApi, Tags, payload::Json}; use serde::Serialize; use std::sync::Arc; #[derive(Tags)] enum AgentsTags { Agents, } #[derive(Object)] struct StartAgentPayload { story_id: String, agent_name: Option, } #[derive(Object)] struct StopAgentPayload { story_id: String, agent_name: String, } #[derive(Object, Serialize)] struct AgentInfoResponse { story_id: String, agent_name: String, status: String, session_id: Option, worktree_path: Option, } #[derive(Object, Serialize)] struct AgentConfigInfoResponse { name: String, role: String, model: Option, allowed_tools: Option>, max_turns: Option, max_budget_usd: Option, } pub struct AgentsApi { pub ctx: Arc, } #[OpenApi(tag = "AgentsTags::Agents")] impl AgentsApi { /// Start an agent for a given story (creates worktree, runs setup, spawns agent). /// If agent_name is omitted, the first configured agent is used. #[oai(path = "/agents/start", method = "post")] async fn start_agent( &self, payload: Json, ) -> OpenApiResult> { let project_root = self .ctx .agents .get_project_root(&self.ctx.state) .map_err(bad_request)?; let info = self .ctx .agents .start_agent( &project_root, &payload.0.story_id, payload.0.agent_name.as_deref(), ) .await .map_err(bad_request)?; Ok(Json(AgentInfoResponse { story_id: info.story_id, agent_name: info.agent_name, status: info.status.to_string(), session_id: info.session_id, worktree_path: info.worktree_path, })) } /// Stop a running agent and clean up its worktree. #[oai(path = "/agents/stop", method = "post")] async fn stop_agent(&self, payload: Json) -> OpenApiResult> { let project_root = self .ctx .agents .get_project_root(&self.ctx.state) .map_err(bad_request)?; self.ctx .agents .stop_agent( &project_root, &payload.0.story_id, &payload.0.agent_name, ) .await .map_err(bad_request)?; Ok(Json(true)) } /// List all agents with their status. #[oai(path = "/agents", method = "get")] async fn list_agents(&self) -> OpenApiResult>> { let agents = self.ctx.agents.list_agents().map_err(bad_request)?; Ok(Json( agents .into_iter() .map(|info| AgentInfoResponse { story_id: info.story_id, agent_name: info.agent_name, status: info.status.to_string(), session_id: info.session_id, worktree_path: info.worktree_path, }) .collect(), )) } /// Get the configured agent roster from project.toml. #[oai(path = "/agents/config", method = "get")] async fn get_agent_config( &self, ) -> OpenApiResult>> { let project_root = self .ctx .agents .get_project_root(&self.ctx.state) .map_err(bad_request)?; let config = ProjectConfig::load(&project_root).map_err(bad_request)?; Ok(Json( config .agent .iter() .map(|a| AgentConfigInfoResponse { name: a.name.clone(), role: a.role.clone(), model: a.model.clone(), allowed_tools: a.allowed_tools.clone(), max_turns: a.max_turns, max_budget_usd: a.max_budget_usd, }) .collect(), )) } /// Reload project config and return the updated agent roster. #[oai(path = "/agents/config/reload", method = "post")] async fn reload_config( &self, ) -> OpenApiResult>> { let project_root = self .ctx .agents .get_project_root(&self.ctx.state) .map_err(bad_request)?; let config = ProjectConfig::load(&project_root).map_err(bad_request)?; Ok(Json( config .agent .iter() .map(|a| AgentConfigInfoResponse { name: a.name.clone(), role: a.role.clone(), model: a.model.clone(), allowed_tools: a.allowed_tools.clone(), max_turns: a.max_turns, max_budget_usd: a.max_budget_usd, }) .collect(), )) } }