loops: bridge research artifact into loop iterations (option C + b)
The bridge lets a coding loop "consume" an integrations research artifact one INT-XX item per iteration. Options b (order-sequential iteration) and C (snapshot in task_template + save the pointer for future refresh) from the design discussion. Migration 0042 — three new loops columns: - source_research_topic_id — nullable pointer to research_topics. - consumed_int_ids TEXT[] — INT-XX ids the loop has completed. Advances when topology_worker parses "COMPLETED: INT-<NN>" markers from the run's final output (wired in a follow-up commit). - current_int_index INT — monotonic pointer for order-sequential iteration. Coordinator addresses INT-<current+1> unless prereqs are unmet, in which case it works on the smallest unblocking INT-XX and logs the reorder rationale. Backend: - cm_db::repo::loops::set_source_research_topic — bind/unbind pointer. - cm_db::repo::loops::source_research_context — read pointer + state. - routes::loops::compose_iteration_task — new caller-side helper that reads the pointer, fetches the topic's latest research_outcome, and prepends the artifact + focus instruction to task_template. - run_now + webhook_receive both pass task_template through compose_iteration_task before enqueue. Standalone loops (no pointer) behave identically to before. - CreateLoopRequest accepts `source_research_topic_id`, ownership- checked via research_topics::get before persist. Frontend: - New ResearchArtifactPicker modal — lists published topics, fetches the artifact on pick, returns (topic_id, markdown) to caller. - LoopsWizard task_template step gains "Import from research artifact" button (right-aligned). Click opens the picker. On pick: task populates with the artifact markdown, pointer saved, textarea expands to 8 rows, small info strip shows "Loop is bound to topic <id>. Each iteration will focus on the next unconsumed INT-XX." - Unlink button reverts to standalone loop mode. Follow-up (next commit): - topology_worker completion hook — parse "COMPLETED: INT-<NN>" out of the run's final output + update consumed_int_ids + current_int_index atomically. Without this, current_int_index stays at 0 forever and every iteration works on the same INT. - Loop card refresh button — re-read source topic's latest outcome (useful after a reject-with-revision cycle on the source topic).
This commit is contained in:
@@ -247,6 +247,59 @@ pub async fn set_zeroclaw_container(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bind (or unbind) a loop's source research topic. When set, the loop's
|
||||
/// enqueue path prepends the topic's latest artifact + a "focus on the
|
||||
/// next unconsumed INT" instruction to the coordinator task (option b,
|
||||
/// order-sequential iteration).
|
||||
pub async fn set_source_research_topic(
|
||||
pool: &PgPool,
|
||||
loop_id: Uuid,
|
||||
workspace_id: Uuid,
|
||||
source: Option<Uuid>,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query(
|
||||
"UPDATE loops
|
||||
SET source_research_topic_id = $3, updated_at = now()
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
)
|
||||
.bind(loop_id)
|
||||
.bind(workspace_id)
|
||||
.bind(source)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a loop's source research topic id + consumed INT ids +
|
||||
/// current index. Used by the enqueue path when building the
|
||||
/// coordinator task string. Missing rows / NULL columns return None
|
||||
/// so the caller can fall back to the plain task_template.
|
||||
pub async fn source_research_context(
|
||||
pool: &PgPool,
|
||||
loop_id: Uuid,
|
||||
) -> Result<Option<(Uuid, Vec<String>, i32)>, DbError> {
|
||||
use sqlx::Row;
|
||||
let row = sqlx::query(
|
||||
"SELECT source_research_topic_id, consumed_int_ids, current_int_index
|
||||
FROM loops
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(loop_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row.and_then(|r| {
|
||||
let topic = r
|
||||
.try_get::<Option<Uuid>, _>("source_research_topic_id")
|
||||
.ok()
|
||||
.flatten()?;
|
||||
let consumed = r
|
||||
.try_get::<Vec<String>, _>("consumed_int_ids")
|
||||
.unwrap_or_default();
|
||||
let idx = r.try_get::<i32, _>("current_int_index").unwrap_or(0);
|
||||
Some((topic, consumed, idx))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Read the per-loop gateway URL (or None if the loop hasn't spawned a
|
||||
/// container yet). Used by `topology_worker` to prefer the isolated
|
||||
/// daemon over the workspace-wide one.
|
||||
|
||||
Reference in New Issue
Block a user