use std::sync::Arc;
use axum::Json;
use axum::extract::State;
use axum::response::IntoResponse;
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::actuator::{InjectOutcome, ManagerActuator, SummarizeOutcome, resolve_actuator};
use crate::daemon::error::DaemonError;
use crate::daemon::state::DaemonState;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ProposedAction {
Launch {
project: String,
task: String,
},
Inject {
session: String,
text: String,
},
Summarize {
session: String,
},
}
#[derive(Debug, Deserialize)]
pub struct ActRequest {
pub conversation_key: String,
pub action: ProposedAction,
#[serde(default)]
pub confirm: bool,
}
#[derive(Debug, Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ActResponse {
Proposed {
proposal: String,
action: ProposedAction,
},
Launched {
session_id: String,
name: String,
state: String,
},
Injected {
session_id: String,
name: String,
text: String,
},
Summarized {
session_id: String,
name: String,
state: String,
summary: String,
pending_decision: Option<String>,
},
SessionNotFound {
session: String,
error: String,
},
SessionVanished {
session: String,
error: String,
},
ActionFailed {
session: String,
error: String,
},
}
impl From<InjectOutcome> for ActResponse {
fn from(o: InjectOutcome) -> Self {
match o {
InjectOutcome::Sent { target, text } => Self::Injected {
session_id: target.id,
name: target.name,
text,
},
InjectOutcome::NotFound { session, error } => Self::SessionNotFound { session, error },
InjectOutcome::Vanished { target, error } => Self::SessionVanished {
session: target.name,
error,
},
InjectOutcome::Failed { target, error } => Self::ActionFailed {
session: target.name,
error,
},
}
}
}
impl From<SummarizeOutcome> for ActResponse {
fn from(o: SummarizeOutcome) -> Self {
match o {
SummarizeOutcome::Summary {
target,
state,
summary,
pending_decision,
} => Self::Summarized {
session_id: target.id,
name: target.name,
state,
summary,
pending_decision,
},
SummarizeOutcome::NotFound { session, error } => {
Self::SessionNotFound { session, error }
}
SummarizeOutcome::Vanished { target, error } => Self::SessionVanished {
session: target.name,
error,
},
SummarizeOutcome::Failed { target, error } => Self::ActionFailed {
session: target.name,
error,
},
}
}
}
pub fn propose_message(action: &ProposedAction) -> String {
let body = match action {
ProposedAction::Launch { project, task } => {
format!("launch a new session for project '{project}' with task: {task:?}")
}
ProposedAction::Inject { session, text } => {
format!("inject into session '{session}' the message: {text:?}")
}
ProposedAction::Summarize { session } => {
format!("summarize the recent activity of session '{session}'")
}
};
format!("Proposed: {body}. Re-send this request with \"confirm\": true to execute it.")
}
pub async fn execute_action(
actuator: &Arc<dyn ManagerActuator>,
conversation_key: &str,
action: ProposedAction,
) -> Result<ActResponse, String> {
match action {
ProposedAction::Launch { project, task } => {
actuator
.launch(&project, &task)
.await
.map(|outcome| ActResponse::Launched {
session_id: outcome.session_id,
name: outcome.name,
state: outcome.state,
})
}
ProposedAction::Inject { session, text } => Ok(ActResponse::from(
actuator.inject(conversation_key, &session, &text).await,
)),
ProposedAction::Summarize { session } => Ok(ActResponse::from(
actuator.summarize(conversation_key, &session).await,
)),
}
}
pub async fn manager_act_route(
State(state): State<Arc<DaemonState>>,
Json(body): Json<ActRequest>,
) -> impl IntoResponse {
match act_op(&state, body).await {
Ok(response) => Json(response).into_response(),
Err(e) => {
let code = match &e {
DaemonError::InvalidRequest(_) => "invalid_request",
_ => "launch_failed",
};
(
e.status(),
Json(json!({ "error": code, "message": e.detail() })),
)
.into_response()
}
}
}
pub async fn act_op(
state: &Arc<DaemonState>,
body: ActRequest,
) -> Result<ActResponse, DaemonError> {
let conversation_key = body.conversation_key.trim().to_string();
if conversation_key.is_empty() {
return Err(DaemonError::InvalidRequest(
"conversation_key must not be empty".to_string(),
));
}
if !body.confirm {
return Ok(ActResponse::Proposed {
proposal: propose_message(&body.action),
action: body.action,
});
}
let actuator = resolve_actuator(state);
execute_action(&actuator, &conversation_key, body.action)
.await
.map_err(|error| {
tracing::warn!("manager act: action execution failed: {error}");
DaemonError::UpstreamFailed(error)
})
}
#[cfg(test)]
#[path = "act_tests.rs"]
mod tests;