research pipeline v2: topology-aware start + persisted draft
Three connected changes that turn "Start research" from a status flip into a real pipeline that produces a reviewable artifact: - Migration 0036: adds research_topics.topology_kind (default 'hub_spoke') and a new research_outcomes table (id, topic_id, version DESC, body_md, produced_by_run_id, created_at) so each run's final synthesis is versioned and persistent. - Wizard now has a topology picker in the Outcome step — hub_spoke / pipeline / hierarchical / star_moe — with copy that steers users to the right shape (Pipeline for research → distill → analyze → implement rosters, hub_spoke for the coordinator- and-specialists default). - start_topic reads the chosen topology_kind, parses it into a cm_topology::TopologyKind, and dispatches a per-shape coordinator prompt via build_coordinator_task. Pipeline explicitly tells stage 1 not to write the final artifact and propagates a "final stage MUST emit a complete markdown document with measurable acceptance criteria" instruction downstream. The graph builder is called with the topology the user actually picked instead of hard-coded HubSpoke. - topology_worker::freeze_research_outcome fires after every successful complete(). It looks up research_topic_id on the run; if set and final_output is non-empty, it inserts a new research_outcomes row (version auto-derived server-side via coalesce(max(version), 0) + 1). Best-effort — a DB hiccup logs but doesn't fail the run. - TopicDetail now includes topology_kind and latest_outcome. ResearchCanvas swaps in the outcome's body_md (rendered as pre-wrap markdown, versioned header, produced-at timestamp) whenever an outcome exists; the original prompt collapses into an "Original prompt" <details> below so it's still one click away. Pre-run topics still show the description as before. Follow-ups still open: reject-with-revision loop feeding the coordinator, publishing → published transition + real artifact export (md / pdf), and an approvals inbox surface for reviewers.
This commit is contained in:
@@ -127,6 +127,7 @@ async fn run_job(
|
||||
if let Err(e) = cm_db::repo::topology_runs::complete(pool, id, &value).await {
|
||||
eprintln!("topology_worker: complete({id}) failed: {e}");
|
||||
}
|
||||
freeze_research_outcome(pool, id, &record.final_output).await;
|
||||
}
|
||||
Err(e) => {
|
||||
// Don't clobber a cancellation (or any already-terminal state) with `failed`.
|
||||
@@ -142,6 +143,30 @@ async fn run_job(
|
||||
maybe_transition_research_topic(pool, id).await;
|
||||
}
|
||||
|
||||
/// If this run belongs to a research topic, snapshot the orchestrator's
|
||||
/// final synthesis as a versioned `research_outcomes` row. The frontend
|
||||
/// canvas reads `latest_outcome` for anything past `standby` so reviewers
|
||||
/// see the produced draft rather than the original prompt. Best-effort:
|
||||
/// a failure here logs but doesn't fail the run.
|
||||
async fn freeze_research_outcome(pool: &PgPool, run_id: Uuid, final_output: &str) {
|
||||
let topic_id = match cm_db::repo::topology_runs::research_topic_id(pool, run_id).await {
|
||||
Ok(Some(id)) => id,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
eprintln!("topology_worker: research_topic_id({run_id}) failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if final_output.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) =
|
||||
cm_db::repo::research_outcomes::insert(pool, topic_id, final_output, Some(run_id)).await
|
||||
{
|
||||
eprintln!("topology_worker: research_outcomes::insert({run_id}) failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Post-terminal hook: if this run belongs to a research topic and it was
|
||||
/// the last sibling in flight, transition the topic `processing → reviewing`.
|
||||
/// Best-effort — a DB hiccup here logs but doesn't fail the run.
|
||||
|
||||
Reference in New Issue
Block a user