master planner: wire user-locked topology_kind through chat + scaffold

Frontend: send topologyKind to /api/planner/chat so the planner's user
prompt gets a USER-LOCKED TOPOLOGY block telling Opus to use it verbatim.
On buildTeam, override proposal.topology_kind with the user's pick
(belt-and-braces — if the planner ignored the lock, we still ship the
right shape). Proposal chip renders the effective kind in a lavender
tint when it was overridden, with a hover title showing what was
replaced.

Backend PlannerChatRequest gains an optional topology_kind. Empty /
absent = planner picks. Not honored for 'swarm' mode (swarm planner
doesn't take a topology kind).
This commit is contained in:
Omar Sobh
2026-07-07 04:23:00 -07:00
parent 5e49086af7
commit b0acdfd987
2 changed files with 32 additions and 4 deletions
+14 -1
View File
@@ -54,6 +54,11 @@ pub struct PlannerChatRequest {
/// Deploy mode: specialists | team | swarm | scheduled | triggered (default specialists).
#[serde(default)]
pub mode: String,
/// User-locked topology from the gallery. When set the planner is told to
/// use this kind verbatim (no rewrite). Ignored for `swarm` mode (the swarm
/// planner doesn't take a topology kind).
#[serde(default)]
pub topology_kind: Option<String>,
}
const SPECIALISTS_NOTE: &str = "\n\nMODE: Specialists. Propose a TIGHTLY-SCOPED group of 2 or 3 DOMAIN \
@@ -113,7 +118,15 @@ pub async fn planner_chat(
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 topology_lock = match body.topology_kind.as_deref() {
Some(k) if !k.is_empty() && body.mode != "swarm" => format!(
"\n\n=== USER-LOCKED TOPOLOGY ===\nThe user has selected topology_kind='{k}' from the gallery. \
Use this kind verbatim in the proposal; do NOT choose a different one. Tailor the roles + \
member count to work well within this shape."
),
_ => String::new(),
};
let user_prompt = format!("{hierarchy}{topology_lock}\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,