agent-envoy 0.2.0

Message/coordination server for AI coding agents using sqlitegraph pub/sub
Documentation
use axum::extract::{Path, Query, State};
use axum::Json;
use serde_json::json;
use std::sync::Arc;

use crate::atheneum_bridge::types::*;
use crate::atheneum_bridge::utils::{entity_to_json, parse_blocker_type, parse_status};
use crate::error::Result;
use crate::http::AppState;

pub async fn post_task(
    State(state): State<Arc<AppState>>,
    Json(req): Json<CreateTaskRequest>,
) -> Result<impl axum::response::IntoResponse> {
    let task_id = state
        .with_atheneum_async(move |g| {
            g.create_task(
                &req.title,
                req.description.as_deref(),
                req.project_id.as_deref(),
            )
            .map_err(crate::error::EnvoyError::from)
        })
        .await?;
    Ok((
        axum::http::StatusCode::CREATED,
        Json(TaskCreatedResponse {
            task_id,
            status: "TODO".to_string(),
        }),
    ))
}

pub async fn get_tasks(
    State(state): State<Arc<AppState>>,
    Query(query): Query<ListTasksQuery>,
) -> Result<impl axum::response::IntoResponse> {
    let project = query.project.clone();
    let status_str = query.status.clone();

    let tasks: Vec<serde_json::Value> = state
        .with_atheneum_async(move |g| {
            let entities = match status_str {
                Some(s) => {
                    let status = parse_status(&s)?;
                    g.list_tasks_by_status(status, project.as_deref())
                        .map_err(crate::error::EnvoyError::from)?
                }
                None => {
                    let all = g
                        .entities_by_kind("Task")
                        .map_err(crate::error::EnvoyError::from)?;
                    all.into_iter()
                        .filter(|t| match &project {
                            None => true,
                            Some(pid) => {
                                t.data.get("project_id").and_then(|v| v.as_str()) == Some(pid)
                            }
                        })
                        .collect()
                }
            };
            Ok(entities.into_iter().map(entity_to_json).collect())
        })
        .await?;

    Ok(Json(ListTasksResponse { tasks }))
}

pub async fn get_task_details_route(
    State(state): State<Arc<AppState>>,
    Path(task_id): Path<i64>,
) -> Result<impl axum::response::IntoResponse> {
    let detail = state
        .with_atheneum_async(move |g| {
            g.get_task_with_details(task_id)
                .map_err(crate::error::EnvoyError::from)
        })
        .await?;
    Ok(Json(TaskDetailResponse {
        task: entity_to_json(detail.task),
        requirements: detail
            .requirements
            .into_iter()
            .map(entity_to_json)
            .collect(),
        blockers: detail.blockers.into_iter().map(entity_to_json).collect(),
    }))
}

pub async fn patch_task_status_route(
    State(state): State<Arc<AppState>>,
    Path(task_id): Path<i64>,
    Json(req): Json<UpdateTaskStatusRequest>,
) -> Result<axum::http::StatusCode> {
    let status = parse_status(&req.status)?;
    state
        .with_atheneum_async(move |g| {
            g.update_task_status(task_id, status)
                .map_err(crate::error::EnvoyError::from)
        })
        .await?;
    Ok(axum::http::StatusCode::OK)
}

pub async fn post_task_requirement(
    State(state): State<Arc<AppState>>,
    Path(task_id): Path<i64>,
    Json(req): Json<CreateRequirementRequest>,
) -> Result<impl axum::response::IntoResponse> {
    let id = state
        .with_atheneum_async(move |g| {
            g.add_requirement(task_id, &req.statement, req.verification_method.as_deref())
                .map_err(crate::error::EnvoyError::from)
        })
        .await?;
    Ok((
        axum::http::StatusCode::CREATED,
        Json(json!({"requirement_id": id})),
    ))
}

pub async fn post_task_blocker(
    State(state): State<Arc<AppState>>,
    Path(task_id): Path<i64>,
    Json(req): Json<CreateBlockerRequest>,
) -> Result<impl axum::response::IntoResponse> {
    let blocker_type = parse_blocker_type(&req.blocker_type)?;
    let id = state
        .with_atheneum_async(move |g| {
            g.add_blocker(task_id, &req.description, blocker_type)
                .map_err(crate::error::EnvoyError::from)
        })
        .await?;
    Ok((
        axum::http::StatusCode::CREATED,
        Json(json!({"blocker_id": id})),
    ))
}

pub async fn post_journal(
    State(state): State<Arc<AppState>>,
    Json(req): Json<IngestJournalRequest>,
) -> Result<impl axum::response::IntoResponse> {
    let (section_ids, applied): (Vec<i64>, Vec<serde_json::Value>) = state
        .with_atheneum_async(move |g| {
            let ids = g
                .ingest_journal(&req.path, &req.content, req.project_id.as_deref())
                .map_err(crate::error::EnvoyError::from)?;
            let mut all_applied: Vec<serde_json::Value> = Vec::new();
            for sid in &ids {
                let applied = g
                    .apply_kanban_updates_from_journal(*sid)
                    .map_err(crate::error::EnvoyError::from)?;
                for u in applied {
                    all_applied.push(json!({
                        "task_id": u.task_id,
                        "task_title": u.task_title,
                        "previous_status": u.previous_status.as_str(),
                        "new_status": u.new_status.as_str(),
                    }));
                }
            }
            Ok((ids, all_applied))
        })
        .await?;

    Ok((
        axum::http::StatusCode::CREATED,
        Json(IngestJournalResponse {
            section_ids,
            applied_kanban_updates: applied,
        }),
    ))
}