Recursive deploy ladder: Company + Org tiers, mesh mark, two-tier rail
Completes the scale ladder (single → team → company → org). Every tier is a
topology whose nodes are the tier below; running a parent recursively runs each
child's sub-topology down to the leaf claws.
Backend:
- migration 0011: companies/company_teams, orgs/org_companies, topology_runs.tier
- cm-db repos for companies + orgs (mirror teams)
- TurnRequest.attrs (forwarded from node.attrs) for child-id binding
- SubTopologyExecutor (recursive_exec.rs): a parent "turn" runs the child's
sub-topology; durability via parent updated_at keepalive + cancel propagation
+ depth cap; boxed future breaks the org→company recursion
- topology_worker selects executor by job.tier
- routes: /api/companies, /api/orgs (create/list/get/run) + unified
/api/structure/{level}/{id} for the zoom canvas
Frontend:
- MeshMark: node-mesh brand glyph (replaces the claw PNG), tier variants
- TopologyGraphView: optional onNodeClick/nodeMeta + dark-token theming
- StructureCanvas + Breadcrumb: one recursive zoom view for every tier
(drill down on node click, breadcrumb up); TeamRunPanel extracted + shared
- two-tier Discord-style rail: StructureRail (mesh mark + org/company/team
glyphs + tools popover + deploy + user) | RosterColumn (selected group's
children, or your claws); SecondaryNav for cross-cutting tools
- ComposeWizard (company/org) wired into DeployWizard; /companies + /orgs pages
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
bba18a4687
commit
3eca4ed70c
@@ -0,0 +1,225 @@
|
||||
//! Company endpoints — deploy a baseline topology staffed with real *teams*,
|
||||
//! then run it on the durable recursive runner. The 3rd rung of the deploy
|
||||
//! ladder (single → team → company → org). Unlike teams, creating a company
|
||||
//! provisions nothing new: it composes teams that already own provisioned claws.
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use cm_topology::{build, TopologyKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
/// One bound team in the company (becomes a topology node).
|
||||
#[derive(Deserialize)]
|
||||
pub struct CompanyMemberInput {
|
||||
pub team_id: Uuid,
|
||||
#[serde(default)]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateCompanyRequest {
|
||||
pub name: String,
|
||||
/// TopologyKind (snake_case), e.g. "hierarchical", "pipeline".
|
||||
pub kind: String,
|
||||
pub members: Vec<CompanyMemberInput>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CompanyCreated {
|
||||
pub company_id: String,
|
||||
}
|
||||
|
||||
fn parse_kind(s: &str) -> Result<TopologyKind, ApiError> {
|
||||
serde_json::from_value(Value::String(s.to_string())).map_err(|_| ApiError::BadRequest)
|
||||
}
|
||||
|
||||
/// `POST /api/companies` — build the baseline topology over the chosen teams,
|
||||
/// bind each node to its team, and persist. Validates every team belongs to the
|
||||
/// workspace first (so a company can only compose teams the caller owns).
|
||||
pub async fn create_company(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Json(body): Json<CreateCompanyRequest>,
|
||||
) -> Result<(StatusCode, Json<CompanyCreated>), ApiError> {
|
||||
if body.members.is_empty() {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
let kind = parse_kind(&body.kind)?;
|
||||
|
||||
// 1. Validate each bound team is real + in this workspace.
|
||||
for m in &body.members {
|
||||
cm_db::repo::teams::get_team(&state.pool, m.team_id, user.workspace_id).await?;
|
||||
}
|
||||
|
||||
// 2. Build the topology and bind each node to its team. We write the team id
|
||||
// into BOTH attrs["team_id"] (descriptive) and attrs["agent"] (so the
|
||||
// orchestrator forwards it through TurnRequest exactly like a claw alias).
|
||||
let roles: Vec<&str> = body
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
if m.role.is_empty() {
|
||||
"team"
|
||||
} else {
|
||||
m.role.as_str()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut graph = build(kind, &roles).map_err(|_| ApiError::BadRequest)?;
|
||||
for (i, node) in graph.nodes.iter_mut().enumerate() {
|
||||
if let Some(m) = body.members.get(i) {
|
||||
node.attrs.insert("team_id".into(), m.team_id.to_string());
|
||||
node.attrs.insert("agent".into(), m.team_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Persist company + node→team bindings.
|
||||
let company_id = Uuid::now_v7();
|
||||
let graph_json = serde_json::to_value(&graph).map_err(|_| ApiError::Internal)?;
|
||||
cm_db::repo::companies::insert_company(
|
||||
&state.pool,
|
||||
company_id,
|
||||
user.workspace_id,
|
||||
&body.name,
|
||||
kind.as_str(),
|
||||
&graph_json,
|
||||
)
|
||||
.await?;
|
||||
for (i, node) in graph.nodes.iter().enumerate() {
|
||||
if let Some(m) = body.members.get(i) {
|
||||
cm_db::repo::companies::add_team(
|
||||
&state.pool,
|
||||
company_id,
|
||||
&node.id,
|
||||
m.team_id,
|
||||
&node.role,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(CompanyCreated {
|
||||
company_id: company_id.to_string(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CompanySummaryOut {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// `GET /api/companies` — recent companies for the workspace.
|
||||
pub async fn list_companies(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
) -> Result<Json<Vec<CompanySummaryOut>>, ApiError> {
|
||||
let rows =
|
||||
cm_db::repo::companies::list_for_workspace(&state.pool, user.workspace_id, 50).await?;
|
||||
Ok(Json(
|
||||
rows.into_iter()
|
||||
.map(|c| CompanySummaryOut {
|
||||
id: c.id.to_string(),
|
||||
name: c.name,
|
||||
kind: c.kind,
|
||||
status: c.status,
|
||||
created_at: c.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CompanyTeamOut {
|
||||
pub node_id: String,
|
||||
pub team_id: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CompanyDetail {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub graph: Value,
|
||||
pub members: Vec<CompanyTeamOut>,
|
||||
}
|
||||
|
||||
/// `GET /api/companies/{id}` — a company's graph + node→team bindings.
|
||||
pub async fn get_company(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<CompanyDetail>, ApiError> {
|
||||
let company = cm_db::repo::companies::get(&state.pool, id, user.workspace_id).await?;
|
||||
let members = cm_db::repo::companies::teams_for_company(&state.pool, id).await?;
|
||||
Ok(Json(CompanyDetail {
|
||||
id: company.id.to_string(),
|
||||
name: company.name,
|
||||
kind: company.kind,
|
||||
status: company.status,
|
||||
created_at: company.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
graph: company.graph,
|
||||
members: members
|
||||
.into_iter()
|
||||
.map(|m| CompanyTeamOut {
|
||||
node_id: m.node_id,
|
||||
team_id: m.team_id.to_string(),
|
||||
role: m.role,
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RunCompanyRequest {
|
||||
pub task: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RunAccepted {
|
||||
pub run_id: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// `POST /api/companies/{id}/run` — enqueue a durable `company`-tier run; the
|
||||
/// worker drives the recursive executor (each node runs its team's topology).
|
||||
pub async fn run_company(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<RunCompanyRequest>,
|
||||
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
|
||||
let company = cm_db::repo::companies::get(&state.pool, id, user.workspace_id).await?;
|
||||
let run_id = Uuid::now_v7();
|
||||
cm_db::repo::topology_runs::enqueue_run_tier(
|
||||
&state.pool,
|
||||
run_id,
|
||||
user.workspace_id,
|
||||
&body.task,
|
||||
&company.graph,
|
||||
"company",
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
Json(RunAccepted {
|
||||
run_id: run_id.to_string(),
|
||||
status: "queued".into(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
@@ -5,15 +5,18 @@ pub mod billing;
|
||||
pub mod browser;
|
||||
pub mod claw_chat;
|
||||
pub mod claws;
|
||||
pub mod companies;
|
||||
pub mod files;
|
||||
pub mod gateway;
|
||||
pub mod health;
|
||||
pub mod identity;
|
||||
pub mod oauth;
|
||||
pub mod orgs;
|
||||
pub mod routines;
|
||||
pub mod sessions;
|
||||
pub mod skills;
|
||||
pub mod slack;
|
||||
pub mod structure;
|
||||
pub mod team;
|
||||
pub mod teams;
|
||||
pub mod topology;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Org endpoints — deploy a baseline topology whose nodes are real *companies*,
|
||||
//! then run it on the durable recursive runner. The top rung of the deploy
|
||||
//! ladder (single → team → company → org). Creating an org composes companies
|
||||
//! that already own their teams (which own their provisioned claws).
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::Json;
|
||||
use cm_topology::{build, TopologyKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
/// One bound company in the org (becomes a topology node).
|
||||
#[derive(Deserialize)]
|
||||
pub struct OrgMemberInput {
|
||||
pub company_id: Uuid,
|
||||
#[serde(default)]
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateOrgRequest {
|
||||
pub name: String,
|
||||
/// TopologyKind (snake_case), e.g. "hierarchical", "pipeline".
|
||||
pub kind: String,
|
||||
pub members: Vec<OrgMemberInput>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrgCreated {
|
||||
pub org_id: String,
|
||||
}
|
||||
|
||||
fn parse_kind(s: &str) -> Result<TopologyKind, ApiError> {
|
||||
serde_json::from_value(Value::String(s.to_string())).map_err(|_| ApiError::BadRequest)
|
||||
}
|
||||
|
||||
/// `POST /api/orgs` — build the baseline topology over the chosen companies,
|
||||
/// bind each node to its company, and persist. Validates ownership first.
|
||||
pub async fn create_org(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Json(body): Json<CreateOrgRequest>,
|
||||
) -> Result<(StatusCode, Json<OrgCreated>), ApiError> {
|
||||
if body.members.is_empty() {
|
||||
return Err(ApiError::BadRequest);
|
||||
}
|
||||
let kind = parse_kind(&body.kind)?;
|
||||
|
||||
// 1. Validate each bound company is real + in this workspace.
|
||||
for m in &body.members {
|
||||
cm_db::repo::companies::get(&state.pool, m.company_id, user.workspace_id).await?;
|
||||
}
|
||||
|
||||
// 2. Build the topology and bind each node to its company (attrs["company_id"]
|
||||
// + attrs["agent"], so the orchestrator forwards it through TurnRequest).
|
||||
let roles: Vec<&str> = body
|
||||
.members
|
||||
.iter()
|
||||
.map(|m| {
|
||||
if m.role.is_empty() {
|
||||
"company"
|
||||
} else {
|
||||
m.role.as_str()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut graph = build(kind, &roles).map_err(|_| ApiError::BadRequest)?;
|
||||
for (i, node) in graph.nodes.iter_mut().enumerate() {
|
||||
if let Some(m) = body.members.get(i) {
|
||||
node.attrs
|
||||
.insert("company_id".into(), m.company_id.to_string());
|
||||
node.attrs.insert("agent".into(), m.company_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Persist org + node→company bindings.
|
||||
let org_id = Uuid::now_v7();
|
||||
let graph_json = serde_json::to_value(&graph).map_err(|_| ApiError::Internal)?;
|
||||
cm_db::repo::orgs::insert_org(
|
||||
&state.pool,
|
||||
org_id,
|
||||
user.workspace_id,
|
||||
&body.name,
|
||||
kind.as_str(),
|
||||
&graph_json,
|
||||
)
|
||||
.await?;
|
||||
for (i, node) in graph.nodes.iter().enumerate() {
|
||||
if let Some(m) = body.members.get(i) {
|
||||
cm_db::repo::orgs::add_company(&state.pool, org_id, &node.id, m.company_id, &node.role)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(OrgCreated {
|
||||
org_id: org_id.to_string(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrgSummaryOut {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// `GET /api/orgs` — recent orgs for the workspace.
|
||||
pub async fn list_orgs(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
) -> Result<Json<Vec<OrgSummaryOut>>, ApiError> {
|
||||
let rows = cm_db::repo::orgs::list_for_workspace(&state.pool, user.workspace_id, 50).await?;
|
||||
Ok(Json(
|
||||
rows.into_iter()
|
||||
.map(|o| OrgSummaryOut {
|
||||
id: o.id.to_string(),
|
||||
name: o.name,
|
||||
kind: o.kind,
|
||||
status: o.status,
|
||||
created_at: o.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrgCompanyOut {
|
||||
pub node_id: String,
|
||||
pub company_id: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct OrgDetail {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub graph: Value,
|
||||
pub members: Vec<OrgCompanyOut>,
|
||||
}
|
||||
|
||||
/// `GET /api/orgs/{id}` — an org's graph + node→company bindings.
|
||||
pub async fn get_org(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<OrgDetail>, ApiError> {
|
||||
let org = cm_db::repo::orgs::get(&state.pool, id, user.workspace_id).await?;
|
||||
let members = cm_db::repo::orgs::companies_for_org(&state.pool, id).await?;
|
||||
Ok(Json(OrgDetail {
|
||||
id: org.id.to_string(),
|
||||
name: org.name,
|
||||
kind: org.kind,
|
||||
status: org.status,
|
||||
created_at: org.created_at.format(&Rfc3339).unwrap_or_default(),
|
||||
graph: org.graph,
|
||||
members: members
|
||||
.into_iter()
|
||||
.map(|m| OrgCompanyOut {
|
||||
node_id: m.node_id,
|
||||
company_id: m.company_id.to_string(),
|
||||
role: m.role,
|
||||
})
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RunOrgRequest {
|
||||
pub task: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct RunAccepted {
|
||||
pub run_id: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// `POST /api/orgs/{id}/run` — enqueue a durable `org`-tier run; the worker
|
||||
/// drives the recursive executor (each node runs its company's topology, which
|
||||
/// runs its teams, which run their claws).
|
||||
pub async fn run_org(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<RunOrgRequest>,
|
||||
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
|
||||
let org = cm_db::repo::orgs::get(&state.pool, id, user.workspace_id).await?;
|
||||
let run_id = Uuid::now_v7();
|
||||
cm_db::repo::topology_runs::enqueue_run_tier(
|
||||
&state.pool,
|
||||
run_id,
|
||||
user.workspace_id,
|
||||
&body.task,
|
||||
&org.graph,
|
||||
"org",
|
||||
)
|
||||
.await?;
|
||||
Ok((
|
||||
StatusCode::ACCEPTED,
|
||||
Json(RunAccepted {
|
||||
run_id: run_id.to_string(),
|
||||
status: "queued".into(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Unified structure endpoint for the recursive zoom canvas.
|
||||
//!
|
||||
//! `GET /api/structure/{level}/{id}` returns one level of the
|
||||
//! `org ▸ company ▸ team ▸ claw` hierarchy in a single polymorphic shape: the
|
||||
//! level's own topology graph plus its children (the tier below) with their
|
||||
//! drill targets. The canvas fetches depth-1 lazily — drilling into a child
|
||||
//! issues another request for that child's level — so a large org never serializes
|
||||
//! its whole subtree at once.
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use cm_domain::AgentId;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{ApiError, AppState, Authed};
|
||||
|
||||
/// One child node (the tier below) with where it drills to.
|
||||
#[derive(Serialize)]
|
||||
pub struct StructureChild {
|
||||
/// The graph node id this child binds to (matches a node in `graph`).
|
||||
pub node_id: String,
|
||||
pub role: String,
|
||||
/// The child's level: "company" | "team" | "claw".
|
||||
pub child_level: String,
|
||||
/// The child's id — the drill target (`/{child_level}s/{child_id}` or, for
|
||||
/// a claw, its chat at `/claws/{child_id}`).
|
||||
pub child_id: String,
|
||||
pub child_name: String,
|
||||
}
|
||||
|
||||
/// One level of the hierarchy: its graph + the children to drill into.
|
||||
#[derive(Serialize)]
|
||||
pub struct StructureNode {
|
||||
/// "org" | "company" | "team" | "claw".
|
||||
pub level: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Topology kind at this level (None for a claw leaf).
|
||||
pub kind: Option<String>,
|
||||
/// This level's TopologyGraph (None for a claw leaf).
|
||||
pub graph: Option<Value>,
|
||||
pub children: Vec<StructureChild>,
|
||||
}
|
||||
|
||||
/// `GET /api/structure/{level}/{id}` — one level of the recursive hierarchy.
|
||||
pub async fn node(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path((level, id)): Path<(String, Uuid)>,
|
||||
) -> Result<Json<StructureNode>, ApiError> {
|
||||
let ws = user.workspace_id;
|
||||
match level.as_str() {
|
||||
"org" => {
|
||||
let org = cm_db::repo::orgs::get(&state.pool, id, ws).await?;
|
||||
let bindings = cm_db::repo::orgs::companies_for_org(&state.pool, id).await?;
|
||||
let mut children = Vec::with_capacity(bindings.len());
|
||||
for b in bindings {
|
||||
let name = cm_db::repo::companies::get(&state.pool, b.company_id, ws)
|
||||
.await
|
||||
.map(|c| c.name)
|
||||
.unwrap_or_default();
|
||||
children.push(StructureChild {
|
||||
node_id: b.node_id,
|
||||
role: b.role,
|
||||
child_level: "company".into(),
|
||||
child_id: b.company_id.to_string(),
|
||||
child_name: name,
|
||||
});
|
||||
}
|
||||
Ok(Json(StructureNode {
|
||||
level: "org".into(),
|
||||
id: org.id.to_string(),
|
||||
name: org.name,
|
||||
kind: Some(org.kind),
|
||||
graph: Some(org.graph),
|
||||
children,
|
||||
}))
|
||||
}
|
||||
"company" => {
|
||||
let company = cm_db::repo::companies::get(&state.pool, id, ws).await?;
|
||||
let bindings = cm_db::repo::companies::teams_for_company(&state.pool, id).await?;
|
||||
let mut children = Vec::with_capacity(bindings.len());
|
||||
for b in bindings {
|
||||
let name = cm_db::repo::teams::get_team(&state.pool, b.team_id, ws)
|
||||
.await
|
||||
.map(|t| t.name)
|
||||
.unwrap_or_default();
|
||||
children.push(StructureChild {
|
||||
node_id: b.node_id,
|
||||
role: b.role,
|
||||
child_level: "team".into(),
|
||||
child_id: b.team_id.to_string(),
|
||||
child_name: name,
|
||||
});
|
||||
}
|
||||
Ok(Json(StructureNode {
|
||||
level: "company".into(),
|
||||
id: company.id.to_string(),
|
||||
name: company.name,
|
||||
kind: Some(company.kind),
|
||||
graph: Some(company.graph),
|
||||
children,
|
||||
}))
|
||||
}
|
||||
"team" => {
|
||||
let team = cm_db::repo::teams::get_team(&state.pool, id, ws).await?;
|
||||
let members = cm_db::repo::teams::members_for_team(&state.pool, id).await?;
|
||||
let mut children = Vec::with_capacity(members.len());
|
||||
for m in members {
|
||||
let name = cm_db::repo::agents::get(&state.pool, AgentId::from(m.claw_id))
|
||||
.await
|
||||
.ok()
|
||||
.filter(|a| a.workspace_id == ws)
|
||||
.map(|a| a.name)
|
||||
.unwrap_or_default();
|
||||
children.push(StructureChild {
|
||||
node_id: m.node_id,
|
||||
role: m.role,
|
||||
child_level: "claw".into(),
|
||||
child_id: m.claw_id.to_string(),
|
||||
child_name: name,
|
||||
});
|
||||
}
|
||||
Ok(Json(StructureNode {
|
||||
level: "team".into(),
|
||||
id: team.id.to_string(),
|
||||
name: team.name,
|
||||
kind: Some(team.kind),
|
||||
graph: Some(team.graph),
|
||||
children,
|
||||
}))
|
||||
}
|
||||
"claw" => {
|
||||
let agent = cm_db::repo::agents::get(&state.pool, AgentId::from(id)).await?;
|
||||
if agent.workspace_id != ws {
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
Ok(Json(StructureNode {
|
||||
level: "claw".into(),
|
||||
id: agent.id.to_string(),
|
||||
name: agent.name,
|
||||
kind: None,
|
||||
graph: None,
|
||||
children: vec![],
|
||||
}))
|
||||
}
|
||||
_ => Err(ApiError::BadRequest),
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,8 @@ pub async fn create_team(
|
||||
.await?;
|
||||
for (i, node) in graph.nodes.iter().enumerate() {
|
||||
if let Some(cid) = claw_ids.get(i) {
|
||||
cm_db::repo::teams::add_member(&state.pool, team_id, &node.id, *cid, &node.role).await?;
|
||||
cm_db::repo::teams::add_member(&state.pool, team_id, &node.id, *cid, &node.role)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,10 @@ pub async fn catalog(_auth: Authed) -> Json<Vec<CatalogEntry>> {
|
||||
}
|
||||
|
||||
/// `POST /api/topologies/classify` — infer a topology kind from a graph.
|
||||
pub async fn classify_graph(_auth: Authed, Json(graph): Json<TopologyGraph>) -> Json<Classification> {
|
||||
pub async fn classify_graph(
|
||||
_auth: Authed,
|
||||
Json(graph): Json<TopologyGraph>,
|
||||
) -> Json<Classification> {
|
||||
Json(classify(&graph))
|
||||
}
|
||||
|
||||
@@ -106,8 +109,7 @@ pub async fn compare_topologies(
|
||||
let judge_spec =
|
||||
std::env::var("CLAWMATES_JUDGE_MODEL").unwrap_or_else(|_| "claude-opus-4-8".to_string());
|
||||
let (judge_provider, judge_model) = state.runtime.resolve_provider(&judge_spec);
|
||||
let executor =
|
||||
ProviderExecutor::new(exec_provider, exec_model, state.runtime.max_tokens());
|
||||
let executor = ProviderExecutor::new(exec_provider, exec_model, state.runtime.max_tokens());
|
||||
let scorer = JudgeScorer::new(judge_provider, judge_model, 16);
|
||||
let cmp = compare(&req.graphs, &req.task, &executor, &scorer)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user