slice 8.5: per-agent + per-team level-up endpoints
Level-up analyzes an agent's brain + recent run outcomes (or a
whole team's aggregate state), calls Gemini 2.5 Flash for structured
JSON proposals, and persists them as pending level_up_proposals
rows. Reviewer approves a subset via /apply; the applier commits
only those items.
Migration 0052 adds level_up_proposals (id, workspace_id, agent_id
XOR team_id via CHECK constraint, status, payload JSONB,
applied_items[], model, created_by, approved_by, created_at,
applied_at) + workspace/pending/agent/team indexes.
Rust surface:
- cm_db::repo::level_up::{insert, get, list_pending, mark_applied,
mark_rejected}
- cm_api::level_up::{propose_agent, propose_team, apply}
Item kinds handled by apply():
identity_refinement → UPDATE agents.system_prompt
skill_add → agent_skills_ext INSERT
skill_candidate → workspace-scoped skills INSERT
(deterministic id per (workspace, name))
brain_consolidation → set_agent_md on the brain (unlike
brain_seed::ingest, this overwrites)
roster_change / mcp_bundle_change — logged as
"not auto-applied, human runs
team-wizard" (structural changes need
human review of side effects).
API:
- POST /api/claws/{id}/level-up → { proposal_id }
- POST /api/teams/{id}/level-up → { proposal_id }
- GET /api/level-up-proposals → pending list
- GET /api/level-up-proposals/{id}
- POST /api/level-up-proposals/{id}/apply { approved_item_ids }
- POST /api/level-up-proposals/{id}/reject
Uses Gemini 2.5 Flash with response_mime_type: "application/json"
so the model returns structured JSON directly (no ```json fence
stripping needed). Configurable via CLAWMATES_LEVEL_UP_MODEL.
Follow-ups:
- Frontend diff-review UI (pick items, approve/reject)
- roster_change / mcp_bundle_change appliers (currently manual)
- Anthropic + OpenAI proposer variants
- Promote workspace-scoped skills to builtin via a curator flow
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
58963d5083
commit
9b5e63cbb7
@@ -0,0 +1,109 @@
|
||||
//! `/api/level-up-proposals/*` + trigger endpoints — Slice 8.5.
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::Json;
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use cm_db::repo::level_up::LevelUpProposal;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
pub async fn list_pending(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
) -> Result<Json<Vec<LevelUpProposal>>, ApiError> {
|
||||
let rows =
|
||||
cm_db::repo::level_up::list_pending(&state.pool, user.workspace_id.as_uuid()).await?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
pub async fn get_proposal(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LevelUpProposal>, ApiError> {
|
||||
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
Ok(Json(p))
|
||||
}
|
||||
|
||||
/// POST /api/claws/{id}/level-up — LLM proposes agent improvements.
|
||||
/// Returns the new proposal id; reviewer approves via the apply route.
|
||||
pub async fn propose_for_agent(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(agent_id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let id = crate::level_up::propose_agent(&state.pool, user.workspace_id, user.user_id, agent_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("level_up: propose_agent {agent_id} failed: {e}");
|
||||
ApiError::Internal
|
||||
})?;
|
||||
Ok(Json(serde_json::json!({ "proposal_id": id })))
|
||||
}
|
||||
|
||||
/// POST /api/teams/{id}/level-up — LLM proposes team improvements.
|
||||
pub async fn propose_for_team(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(team_id): Path<Uuid>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let id = crate::level_up::propose_team(&state.pool, user.workspace_id, user.user_id, team_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("level_up: propose_team {team_id} failed: {e}");
|
||||
ApiError::Internal
|
||||
})?;
|
||||
Ok(Json(serde_json::json!({ "proposal_id": id })))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ApplyRequest {
|
||||
/// Ids from payload.suggested_items[] that the reviewer approved.
|
||||
pub approved_item_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn apply_proposal(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<ApplyRequest>,
|
||||
) -> Result<Json<LevelUpProposal>, ApiError> {
|
||||
crate::level_up::apply(
|
||||
&state.pool,
|
||||
user.workspace_id,
|
||||
user.user_id,
|
||||
id,
|
||||
&body.approved_item_ids,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("level_up::apply {id} failed: {e}");
|
||||
ApiError::Internal
|
||||
})?;
|
||||
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
Ok(Json(p))
|
||||
}
|
||||
|
||||
pub async fn reject_proposal(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<LevelUpProposal>, ApiError> {
|
||||
cm_db::repo::level_up::mark_rejected(
|
||||
&state.pool,
|
||||
id,
|
||||
user.workspace_id.as_uuid(),
|
||||
user.user_id.as_uuid(),
|
||||
)
|
||||
.await?;
|
||||
let p = cm_db::repo::level_up::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
Ok(Json(p))
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod files;
|
||||
pub mod gateway;
|
||||
pub mod health;
|
||||
pub mod identity;
|
||||
pub mod level_up;
|
||||
pub mod loops;
|
||||
pub mod missions;
|
||||
pub mod nodes;
|
||||
|
||||
Reference in New Issue
Block a user