loops: kind='research' dispatch — research runs as loops
Delivers the research/loop fold: kind='research' loops run the research pipeline each iteration, appending a new research_outcomes version. The paired on_artifact_update fan-out then wakes up any kind='exec' loops bound to the same topic to consume new INTs. Every runnable thing is now a loop (D1). Backend — DB helpers: - cm_db::repo::loops::kind_and_binding — reads (kind, source_topic, task_template) so callers can dispatch without hydrating the whole Loop struct. - cm_db::repo::loops::enqueue_iteration_with_topic — new variant that sets research_topic_id on topology_runs alongside loop_id, so the completion hook's freeze_research_outcome writes a new outcome version for research-kind iterations. - cm_db::repo::research_topics::get_any_workspace — cross-workspace fetch used by the research task builder (the loop row is authoritative for the workspace binding via kind_and_binding). Backend — dispatch: - routes::loops::compose_research_iteration_task — builds the coordinator prompt for a research iteration: topic title + description + outcome_kind + prior version pointer + refresh instructions (survey new sources, preserve stable INT ids, mark superseded items as deprecated rather than delete). The completion hook writes the resulting synthesis as research_outcomes v(prior+1). - routes::loops::compose_and_enqueue_iteration — one-shot dispatch: reads the kind, picks compose_iteration_task (exec) or compose_research_iteration_task (research), enqueues with or without research_topic_id set. All four enqueue callsites now route through compose_and_enqueue: - create_loop (initial_burst) - run_now - webhook_receive - topology_worker::continue_initial_burst - topology_worker::freeze_research_outcome (on_artifact_update fan-out) Research-kind loops naturally form the "nightly refresh" side of a paired research + coding loop: research writes a fresh outcome version → fan-out wakes exec loops with on_artifact_update → coding loops consume the next INT (which the research loop may have just added). D2 answer (inherit repo binding): repo lives on the topic; both loops sharing the source topic id read from the same context, no duplication. D3 answer (coordinator resolves): the research iteration prompt tells the team to preserve stable INT ids and mark deprecations rather than delete, so coding loops' consumed lists stay valid across versions. Follow-up (next commit): ResearchWizard schedule step — "Just once / Nightly / Manual" that creates the paired research-kind loop with initial_burst=1 (just once) or cron 0 3 * * * (nightly) + optional paired coding loop with on_artifact_update.
This commit is contained in:
@@ -226,26 +226,20 @@ async fn freeze_research_outcome(pool: &PgPool, run_id: Uuid, final_output: &str
|
||||
{
|
||||
continue; // Coalesce.
|
||||
}
|
||||
let iter = cm_db::repo::loops::next_iteration(pool, loop_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
// Build the enriched task with the freshly-inserted artifact
|
||||
// (compose_iteration_task reads latest, which is what we just
|
||||
// wrote).
|
||||
let task =
|
||||
crate::routes::loops::compose_iteration_task(pool, loop_id, &task_template).await;
|
||||
if let Err(e) = cm_db::repo::loops::enqueue_iteration(
|
||||
// Fan-outs always target kind='exec' (filter enforced in
|
||||
// loops_awaiting_topic). compose_and_enqueue_iteration takes
|
||||
// the exec path and prepends the freshly-inserted artifact.
|
||||
if let Err(e) = crate::routes::loops::compose_and_enqueue_iteration(
|
||||
pool,
|
||||
loop_id,
|
||||
workspace_id,
|
||||
&task,
|
||||
&graph,
|
||||
iter,
|
||||
Some(run_id),
|
||||
Some(&task_template),
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("topology_worker: on_artifact_update enqueue({loop_id}) failed: {e}");
|
||||
eprintln!("topology_worker: on_artifact_update enqueue({loop_id}) failed: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,27 +269,24 @@ async fn continue_initial_burst(pool: &PgPool, run_id: Uuid) {
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Fetch the loop so we have the workspace + graph + task_template
|
||||
// to enqueue the next iteration.
|
||||
// Fetch the loop so we have the workspace + graph. The kind-aware
|
||||
// dispatcher pulls task_template + kind from the same helper it
|
||||
// uses at first fire, so bursts across an exec + research pair
|
||||
// behave identically.
|
||||
let Ok(Some(l)) = cm_db::repo::loops::get_any_workspace(pool, loop_id).await else {
|
||||
return;
|
||||
};
|
||||
let iter = cm_db::repo::loops::next_iteration(pool, loop_id)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let task = crate::routes::loops::compose_iteration_task(pool, loop_id, &l.task_template).await;
|
||||
if let Err(e) = cm_db::repo::loops::enqueue_iteration(
|
||||
if let Err(e) = crate::routes::loops::compose_and_enqueue_iteration(
|
||||
pool,
|
||||
loop_id,
|
||||
l.workspace_id,
|
||||
&task,
|
||||
&l.graph,
|
||||
iter,
|
||||
Some(run_id),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("topology_worker: continue_initial_burst enqueue failed: {e}");
|
||||
eprintln!("topology_worker: continue_initial_burst enqueue failed: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user