Large World graph, agent platform, brain stack & dashboard rebuild

Frontend
- Large World: collapse org/company/team tiers into one expandable React Flow
  hierarchy (WorldFlow) with per-click expand, persisted node positions, a
  compact tree sidebar, wrench multi-select delete across levels, and a sized
  right slide-out (phone/tablet/full) showing an agent summary + drill button.
- Agent page: GitHub-style animated contribution grid (VitalsCard), collapsible
  System Prompt + Personality cards, restructured anatomy cards, bigger avatar
  with name/title header row, Markdown/JSON-aware rendering, brain registry +
  history, avatar generate/upload.
- User-icon menu (Infrastructure/Brains/Tools/Profile/Credits) + ToolPanel;
  Master Planner deploy wizard (Specialists/Swarm/Scheduled/Triggered);
  Team Runs view; reap-progress modal; dashboard is the single live interface.

Backend
- cm-brain crate (.brain as the agent definition) + brain apply/history.
- Hard-purge reap (FK-ordered) + sandbox release + SSE batch-delete.
- Swarm self-verifying loop, mode-aware planner, web.search tool, webhooks
  (migration 0013), org/company/team delete endpoints, scheduler sweeps.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-22 23:21:54 -07:00
co-authored by Claude Opus 4.8
parent 9f266d5806
commit 34f744734b
123 changed files with 9591 additions and 1098 deletions
+225
View File
@@ -0,0 +1,225 @@
//! Master Planner — a chat with Claude Opus 4.8 that proposes a team of agents
//! (named, role'd, one model each) and then scaffolds it end-to-end: creates the
//! team + topology, refines + attaches a brain per agent, and sets up the nightly
//! loop. Replaces the old "+" deploy wizard.
use axum::extract::State;
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::Json;
use serde::Deserialize;
use serde_json::{json, Value};
use std::convert::Infallible;
use crate::routes::claws::{apply_reference_to_claw, enhance_and_publish, extract_json};
use crate::routes::teams::{build_team, TeamMemberInput};
use crate::{AppState, Authed};
fn sse(v: Value) -> Result<Event, Infallible> {
Ok(Event::default().data(v.to_string()))
}
const PLANNER_SYSTEM: &str = "You are the ClawMates Master Planner. You design teams of AI agents to \
accomplish a user's goal across the platform's hierarchy (organizations → companies → teams → agents). \
Have a brief, helpful conversation to understand the goal, then propose a concrete team. For the proposal: \
choose a sensible number of agents (usually 26), each with a UNIQUE human name, a clear ROLE, the best \
MODEL for that role (see catalog), a short brain_query (one domain keyword to fetch a starting brain from \
the registry, e.g. 'research', 'writing', 'data', 'security'), a focused system_prompt, and a one-line \
rationale. Pick a topology_kind from: hub_spoke, hierarchical, pipeline, mesh, flat. If the goal implies \
recurring/autonomous work (e.g. 'nightly'), include a schedule {cron (5-field), prompt (the mission the \
lead agent runs each cycle)}. Use web search to ground version-sensitive or current-fact claims. \
ALWAYS respond with STRICT JSON ONLY (no prose, no markdown), exactly: \
{\"reply\":\"<concise message to the user>\",\"proposal\":null|{\"team_name\":\"...\",\
\"topology_kind\":\"hub_spoke\",\"schedule\":null|{\"cron\":\"0 2 * * *\",\"prompt\":\"...\"},\
\"members\":[{\"name\":\"...\",\"role\":\"...\",\"model\":\"...\",\"brain_query\":\"...\",\
\"system_prompt\":\"...\",\"rationale\":\"...\"}]}}. Set proposal to null while still clarifying; include \
it once you have a concrete team. \n\nMODELS (set each member's \"model\" to exactly one token):\n\
- claude — Claude Opus 4.8: strongest reasoning/planning; coordinators, hard analysis. Highest cost.\n\
- glm-4.7 — strong general reasoning (Z.ai); best cost/quality default for most workers.\n\
- glm-5.2 — GLM Opus-class for the hardest reasoning roles; higher cost.\n\
- kimi — excellent for code-heavy roles.\n\
- gemini — Gemini 2.5 Flash: very fast; classification, summarization, high-volume tasks.\n\
- groq — fastest/cheapest; simple sequential high-throughput steps.\n\
AGENT TOOLS each agent can use at runtime: web.search (find sources), browser.goto (fetch a URL), \
files.write (build a markdown vault in the shared drive), chat.send (delegate to teammates), \
routine.schedule (self-schedule).";
#[derive(Deserialize)]
pub struct PlannerMessage {
pub role: String,
pub content: String,
}
#[derive(Deserialize)]
pub struct PlannerChatRequest {
pub messages: Vec<PlannerMessage>,
/// Deploy mode: specialists | swarm | scheduled | triggered (default specialists).
#[serde(default)]
pub mode: String,
}
const SPECIALISTS_NOTE: &str = "\n\nMODE: Specialists. Each member is a DOMAIN SPECIALIST — set each member's \
brain_query to a concrete domain brain keyword (e.g. 'rust-2024', 'react-native', 'pentest-web', 'db-ops').";
const SCHEDULED_NOTE: &str = "\n\nMODE: Scheduled. Include a schedule in the proposal. Recurring: \
{\"cron\":\"<5-field>\",\"prompt\":\"<mission run each cycle>\"}. One-time: \
{\"one_shot_at\":\"<RFC3339 UTC datetime>\",\"prompt\":\"<mission>\"}.";
const TRIGGERED_NOTE: &str = "\n\nMODE: Triggered. The team will be fired by a webhook on demand. Set the \
schedule field to {\"prompt\":\"<the default task the webhook runs>\"} (NO cron / one_shot_at).";
const SWARM_SYSTEM: &str = "You are the planner for a self-verifying agent SWARM (Opus plans + verifies, a worker \
swarm executes, the loop repeats until every output passes). The user describes a job; you turn it into a swarm \
spec. The CHECKLIST is the verification contract — each item must be objectively checkable per task (e.g. 'states \
a revenue figure', 'cites a resolvable source URL', 'no field left empty'). Have a brief conversation, then emit \
the spec. ALWAYS respond with STRICT JSON ONLY: {\"reply\":\"<concise message>\",\"swarm\":null|{\"goal\":\"<the \
decomposable job>\",\"checklist\":[\"...\",\"...\"],\"task_count\":<int>,\"worker_model\":\"auto\"}}. Set swarm to \
null while still clarifying; include it once the job + checklist are concrete.";
fn planner_system_for(mode: &str) -> String {
match mode {
"swarm" => SWARM_SYSTEM.to_string(),
"scheduled" => format!("{PLANNER_SYSTEM}{SCHEDULED_NOTE}"),
"triggered" => format!("{PLANNER_SYSTEM}{TRIGGERED_NOTE}"),
_ => format!("{PLANNER_SYSTEM}{SPECIALISTS_NOTE}"),
}
}
/// `POST /api/planner/chat` — one planner turn (SSE: thinking → done{reply,proposal}).
pub async fn planner_chat(
State(state): State<AppState>,
Authed(user): Authed,
Json(body): Json<PlannerChatRequest>,
) -> impl axum::response::IntoResponse {
let runtime = state.runtime.clone();
let pool = state.pool.clone();
let ws = user.workspace_id;
let stream = async_stream::stream! {
yield sse(json!({"stage":"thinking","label":"Planning with Claude Opus 4.8…"}));
let agents = cm_db::repo::agents::roster(&pool, ws).await.unwrap_or_default();
let teams = cm_db::repo::teams::list_for_workspace(&pool, ws, 100).await.unwrap_or_default();
let agent_names: Vec<String> = agents.iter().take(40).map(|a| format!("{} ({})", a.name, a.job_title)).collect();
let team_names: Vec<String> = teams.iter().take(40).map(|t| t.name.clone()).collect();
let hierarchy = format!(
"CURRENT WORKSPACE: {} agents, {} teams.\nExisting agents: {}\nExisting teams: {}",
agents.len(), teams.len(),
if agent_names.is_empty() { "(none)".to_string() } else { agent_names.join(", ") },
if team_names.is_empty() { "(none)".to_string() } else { team_names.join(", ") },
);
let convo = body.messages.iter()
.map(|m| format!("{}: {}", if m.role == "user" { "USER" } else { "PLANNER" }, m.content))
.collect::<Vec<_>>().join("\n\n");
let user_prompt = format!("{hierarchy}\n\n=== CONVERSATION ===\n{convo}\n\nRespond now (JSON only).");
let system = planner_system_for(&body.mode);
let raw = match runtime.complete(&system, &user_prompt, "claude-opus-4-8", 8000, true).await {
Ok(t) => t,
Err(e) => { yield sse(json!({"stage":"error","label":format!("Opus error: {e}")})); return; }
};
match extract_json(&raw) {
Some(v) => {
let reply = v.get("reply").and_then(|x| x.as_str()).unwrap_or("").to_string();
let proposal = v.get("proposal").cloned().unwrap_or(Value::Null);
let swarm = v.get("swarm").cloned().unwrap_or(Value::Null);
yield sse(json!({"stage":"done","reply":reply,"proposal":proposal,"swarm":swarm}));
}
None => yield sse(json!({"stage":"done","reply":raw,"proposal":Value::Null,"swarm":Value::Null})),
}
};
Sse::new(Box::pin(stream)).keep_alive(KeepAlive::new())
}
#[derive(Deserialize)]
pub struct ScaffoldMember {
pub name: String,
pub role: String,
#[serde(default)]
pub model: String,
#[serde(default)]
pub brain_query: String,
#[serde(default)]
pub system_prompt: String,
}
#[derive(Deserialize)]
pub struct ScaffoldSchedule {
#[serde(default)]
pub cron: String,
/// One-shot fire time (RFC3339 UTC). When set, takes precedence over `cron`.
#[serde(default)]
pub one_shot_at: String,
pub prompt: String,
}
#[derive(Deserialize)]
pub struct ScaffoldRequest {
pub team_name: String,
#[serde(default = "default_kind")]
pub topology_kind: String,
#[serde(default)]
pub schedule: Option<ScaffoldSchedule>,
pub members: Vec<ScaffoldMember>,
}
fn default_kind() -> String {
"hub_spoke".to_string()
}
/// `POST /api/planner/scaffold` — build the approved team (SSE progress): create
/// agents + topology, refine+attach a brain per agent, set up the nightly loop.
pub async fn planner_scaffold(
State(state): State<AppState>,
Authed(user): Authed,
Json(body): Json<ScaffoldRequest>,
) -> impl axum::response::IntoResponse {
let runtime = state.runtime.clone();
let stream = async_stream::stream! {
if body.members.is_empty() {
yield sse(json!({"stage":"error","pct":100,"label":"Empty proposal"}));
return;
}
let n = body.members.len().max(1);
yield sse(json!({"stage":"team","pct":8,"label":format!("Creating team “{}” ({} agents)…", body.team_name, n)}));
let members: Vec<TeamMemberInput> = body.members.iter().map(|m| TeamMemberInput {
role: m.role.clone(),
name: m.name.clone(),
model: if m.model.trim().is_empty() { "claude".to_string() } else { m.model.clone() },
system_prompt: m.system_prompt.clone(),
accent: String::new(),
}).collect();
let (team_id, claw_ids) = match build_team(&state, user.workspace_id, user.user_id, &body.team_name, &body.topology_kind, &members).await {
Ok(r) => r,
Err(_) => { yield sse(json!({"stage":"error","pct":100,"label":"Team creation failed"})); return; }
};
for (i, (cid, m)) in claw_ids.iter().zip(body.members.iter()).enumerate() {
let pct = 15 + (i as u32) * 70 / (n as u32);
yield sse(json!({"stage":"brain","pct":pct,"label":format!("Refining brain for {} ({})…", m.name, m.role)}));
let q = if m.brain_query.trim().is_empty() { m.role.clone() } else { m.brain_query.clone() };
let found = cm_brain::hub::list(&q).await.unwrap_or_default().into_iter().next().map(|b| b.reference);
if let Some(reference) = found {
let role_ctx = format!("Agent '{}', role '{}', on team '{}'. Mission: {}", m.name, m.role, body.team_name, m.system_prompt);
let refined = enhance_and_publish(&runtime, &reference, &role_ctx).await.unwrap_or(reference);
let id = cm_domain::AgentId::from(*cid);
let _ = apply_reference_to_claw(&state, id, &refined).await;
}
}
if let Some(sch) = &body.schedule {
// A `topology` action fires the whole team's stored graph as a durable
// run (not just one message to the lead).
if let Some(cid) = claw_ids.first() {
let id = cm_domain::AgentId::from(*cid);
let one_shot_at = sch.one_shot_at.trim();
if !one_shot_at.is_empty() {
// One-shot: fire once at the given datetime, never reschedule.
if let Ok(when) = time::OffsetDateTime::parse(one_shot_at, &time::format_description::well_known::Rfc3339) {
yield sse(json!({"stage":"routine","pct":92,"label":"Scheduling the one-time run…"}));
let action = json!({"topology": {"team_id": team_id.to_string(), "task": sch.prompt}, "one_shot": true});
let _ = cm_db::repo::routines::create(&state.pool, id, "Scheduled run", "0 0 1 1 *", action, when).await;
}
} else if !sch.cron.trim().is_empty() {
if let Ok(next) = cm_scheduler::next_occurrence(&sch.cron, time::OffsetDateTime::now_utc()) {
yield sse(json!({"stage":"routine","pct":92,"label":"Scheduling the recurring team loop…"}));
let action = json!({"topology": {"team_id": team_id.to_string(), "task": sch.prompt}});
let _ = cm_db::repo::routines::create(&state.pool, id, "Scheduled team loop", &sch.cron, action, next).await;
}
}
}
}
yield sse(json!({"stage":"done","pct":100,"label":"Team deployed","team_id":team_id.to_string()}));
};
Sse::new(Box::pin(stream)).keep_alive(KeepAlive::new())
}