Use LLM-authored agent todos
This commit is contained in:
@@ -112,16 +112,14 @@ fn exact_provider_action_correlation_is_idempotent() {
|
|||||||
let execution_ref = ProviderToolExecutionRef::new(conversation_id, &batch.work_id, "first");
|
let execution_ref = ProviderToolExecutionRef::new(conversation_id, &batch.work_id, "first");
|
||||||
let existing = HashMap::from([((conversation_id, actions[0].id.clone()), execution_ref)]);
|
let existing = HashMap::from([((conversation_id, actions[0].id.clone()), execution_ref)]);
|
||||||
|
|
||||||
assert!(
|
assert!(BlocklistAIActionModel::provider_action_ids_to_enqueue_from(
|
||||||
BlocklistAIActionModel::provider_action_ids_to_enqueue_from(
|
&existing,
|
||||||
&existing,
|
&actions,
|
||||||
&actions,
|
conversation_id,
|
||||||
conversation_id,
|
&batch,
|
||||||
&batch,
|
)
|
||||||
)
|
.unwrap()
|
||||||
.unwrap()
|
.is_empty());
|
||||||
.is_empty()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -55,15 +55,18 @@ pub(crate) struct ProviderRunResponseProjector {
|
|||||||
translator: RuntimeResponseTranslator,
|
translator: RuntimeResponseTranslator,
|
||||||
has_started_model_turn: bool,
|
has_started_model_turn: bool,
|
||||||
todo_phase: usize,
|
todo_phase: usize,
|
||||||
|
todo_started: bool,
|
||||||
finished: bool,
|
finished: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderRunResponseProjector {
|
impl ProviderRunResponseProjector {
|
||||||
pub(crate) fn new(config: RuntimeResponseConfig) -> Self {
|
pub(crate) fn new(config: RuntimeResponseConfig) -> Self {
|
||||||
|
let todo_started = config.todo_items.is_some();
|
||||||
Self {
|
Self {
|
||||||
translator: RuntimeResponseTranslator::new(config),
|
translator: RuntimeResponseTranslator::new(config),
|
||||||
has_started_model_turn: false,
|
has_started_model_turn: false,
|
||||||
todo_phase: 0,
|
todo_phase: 0,
|
||||||
|
todo_started,
|
||||||
finished: false,
|
finished: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,7 @@ impl ProviderRunResponseProjector {
|
|||||||
has_started_model_turn: false,
|
has_started_model_turn: false,
|
||||||
// Task-list events are part of the already persisted projection.
|
// Task-list events are part of the already persisted projection.
|
||||||
todo_phase: usize::MAX,
|
todo_phase: usize::MAX,
|
||||||
|
todo_started: true,
|
||||||
finished: false,
|
finished: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +110,22 @@ impl ProviderRunResponseProjector {
|
|||||||
}
|
}
|
||||||
ProviderRunProjection::ModelTurnRequested { .. }
|
ProviderRunProjection::ModelTurnRequested { .. }
|
||||||
| ProviderRunProjection::ModelTurnFinished { .. } => Ok(Vec::new()),
|
| ProviderRunProjection::ModelTurnFinished { .. } => Ok(Vec::new()),
|
||||||
ProviderRunProjection::ToolBatchReady { .. } => {
|
ProviderRunProjection::ToolBatchReady { batch } => {
|
||||||
|
if !self.todo_started {
|
||||||
|
if let Some(todos) = todos_from_plan_batch(&batch) {
|
||||||
|
self.todo_started = true;
|
||||||
|
self.todo_phase = 1;
|
||||||
|
return Ok(vec![build_todo_update(
|
||||||
|
&self.translator.config.task_id,
|
||||||
|
api::message::update_todos::Operation::CreateTodoList(
|
||||||
|
api::CreateTodoList {
|
||||||
|
initial_todos: todos,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
let todo_index = self.todo_phase.saturating_sub(1);
|
let todo_index = self.todo_phase.saturating_sub(1);
|
||||||
let Some(todo) = self.todo_items().get(todo_index).cloned() else {
|
let Some(todo) = self.todo_items().get(todo_index).cloned() else {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@@ -173,21 +192,7 @@ impl ProviderRunResponseProjector {
|
|||||||
// Keep the direct-provider workflow visible in the existing task list protocol. These are
|
// Keep the direct-provider workflow visible in the existing task list protocol. These are
|
||||||
// response events, so the normal history model remains the sole owner of task-list state.
|
// response events, so the normal history model remains the sole owner of task-list state.
|
||||||
fn todo_phase_events(&mut self) -> Vec<ResponseEvent> {
|
fn todo_phase_events(&mut self) -> Vec<ResponseEvent> {
|
||||||
let events = match self.todo_phase {
|
Vec::new()
|
||||||
0 => vec![build_todo_update(
|
|
||||||
&self.translator.config.task_id,
|
|
||||||
api::message::update_todos::Operation::CreateTodoList(api::CreateTodoList {
|
|
||||||
initial_todos: self.todo_items(),
|
|
||||||
}),
|
|
||||||
)],
|
|
||||||
_ => Vec::new(),
|
|
||||||
};
|
|
||||||
// The first model turn owns the first phase. Tool batches advance it; this keeps
|
|
||||||
// arbitrary plan lengths aligned with the UpdateTodos protocol.
|
|
||||||
if self.todo_phase == 0 {
|
|
||||||
self.todo_phase = 1;
|
|
||||||
}
|
|
||||||
events
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn todo_completion_events(&self) -> Vec<ResponseEvent> {
|
fn todo_completion_events(&self) -> Vec<ResponseEvent> {
|
||||||
@@ -222,49 +227,59 @@ impl ProviderRunResponseProjector {
|
|||||||
.config
|
.config
|
||||||
.todo_items
|
.todo_items
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(default_workflow_todos)
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_workflow_todos() -> Vec<api::TodoItem> {
|
fn todos_from_plan_batch(
|
||||||
[
|
batch: &galaxy_agent_core::PendingToolBatch,
|
||||||
api::TodoItem {
|
) -> Option<Vec<api::TodoItem>> {
|
||||||
id: "direct-provider-research".to_owned(),
|
let plan_call = batch.calls.iter().find(|pending| {
|
||||||
title: "Research the request".to_owned(),
|
matches!(
|
||||||
description: "Inspect the repository and gather relevant evidence".to_owned(),
|
pending.call.name.as_str(),
|
||||||
},
|
"create_plan" | "create_documents"
|
||||||
api::TodoItem {
|
)
|
||||||
id: "direct-provider-plan".to_owned(),
|
})?;
|
||||||
title: "Create an implementation plan".to_owned(),
|
let documents = plan_call.call.arguments.get("documents")?.as_array()?;
|
||||||
description: "Choose an approach grounded in the repository".to_owned(),
|
let content = documents.first()?.get("content")?.as_str()?;
|
||||||
},
|
let section = content
|
||||||
api::TodoItem {
|
.split_once("## Tasks")
|
||||||
id: "direct-provider-critique".to_owned(),
|
.or_else(|| content.split_once("## Implementation Tasks"))
|
||||||
title: "Critique the approach".to_owned(),
|
.map(|(_, section)| section)
|
||||||
description: "Check assumptions, risks, and missing cases".to_owned(),
|
.unwrap_or(content);
|
||||||
},
|
let todos = section
|
||||||
api::TodoItem {
|
.lines()
|
||||||
id: "direct-provider-revise".to_owned(),
|
.filter_map(|line| {
|
||||||
title: "Revise the plan".to_owned(),
|
let item = line
|
||||||
description: "Incorporate findings before editing".to_owned(),
|
.trim()
|
||||||
},
|
.strip_prefix("- [ ]")
|
||||||
api::TodoItem {
|
.or_else(|| line.trim().strip_prefix("-"))?
|
||||||
id: "direct-provider-implement".to_owned(),
|
.trim();
|
||||||
title: "Implement the change".to_owned(),
|
if item.is_empty() {
|
||||||
description: "Make the requested edits".to_owned(),
|
return None;
|
||||||
},
|
}
|
||||||
api::TodoItem {
|
let title = item
|
||||||
id: "direct-provider-verify".to_owned(),
|
.split_once(" - ")
|
||||||
title: "Verify the result".to_owned(),
|
.map_or(item, |(title, _)| title)
|
||||||
description: "Run proportionate checks".to_owned(),
|
.trim();
|
||||||
},
|
let id = format!(
|
||||||
api::TodoItem {
|
"plan-{}",
|
||||||
id: "direct-provider-repair".to_owned(),
|
title
|
||||||
title: "Repair validation issues".to_owned(),
|
.chars()
|
||||||
description: "Fix failures found during verification".to_owned(),
|
.filter_map(|character| character
|
||||||
},
|
.is_ascii_alphanumeric()
|
||||||
]
|
.then_some(character.to_ascii_lowercase()))
|
||||||
.to_vec()
|
.collect::<String>()
|
||||||
|
);
|
||||||
|
Some(api::TodoItem {
|
||||||
|
id,
|
||||||
|
title: title.to_owned(),
|
||||||
|
description: item.to_owned(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.take(50)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(!todos.is_empty()).then_some(todos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_todo_update(
|
fn build_todo_update(
|
||||||
|
|||||||
@@ -228,14 +228,15 @@ fn prepare_rig_turn_for_provider(
|
|||||||
..
|
..
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let todo_items = todo_items_from_tasks(&tasks);
|
|
||||||
|
|
||||||
let task_id = root_task_id
|
let task_id = root_task_id
|
||||||
.or_else(|| tasks.first().map(|task| task.id.clone()))
|
.or_else(|| tasks.first().map(|task| task.id.clone()))
|
||||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||||
let needs_create_task = tasks.is_empty();
|
let needs_create_task = tasks.is_empty();
|
||||||
let user_query = input.iter().find_map(input_user_query);
|
let user_query = input.iter().find_map(input_user_query);
|
||||||
let mode = mode_override.unwrap_or_else(|| request_mode(&input));
|
let mode = mode_override.unwrap_or_else(|| request_mode(&input));
|
||||||
|
// Only an LLM-authored UpdateTodos message creates the checklist. Ordinary turns
|
||||||
|
// and orchestration prompts must not receive a fabricated plan.
|
||||||
|
let todo_items = todo_items_from_tasks(&tasks);
|
||||||
let available_tools = match mode {
|
let available_tools = match mode {
|
||||||
RigRequestMode::Cli => supported_cli_agent_tools,
|
RigRequestMode::Cli => supported_cli_agent_tools,
|
||||||
RigRequestMode::CompletedCommandAssessment => Vec::new(),
|
RigRequestMode::CompletedCommandAssessment => Vec::new(),
|
||||||
@@ -920,7 +921,7 @@ fn build_system_prompt(
|
|||||||
match mode {
|
match mode {
|
||||||
RigRequestMode::Normal => {}
|
RigRequestMode::Normal => {}
|
||||||
RigRequestMode::Plan => prompt.push_str(
|
RigRequestMode::Plan => prompt.push_str(
|
||||||
"## Plan Mode\nInspect and produce an implementation-ready plan. Do not edit files or perform state-changing actions. Research as needed, then finish by calling `create_plan` to write the plan with the built-in planning tools. If a plan document already exists for this task, call `edit_plan` instead. Do not return the plan only as prose, and do not claim completion until the plan tool succeeds.\n\n",
|
"## Plan Mode\nInspect and produce an implementation-ready plan. Do not edit files or perform state-changing actions. Research as needed, then finish by calling `create_plan` to write the plan with the built-in planning tools. Include a concise `## Tasks` section in the document with short, specific `- [ ]` items. If a plan document already exists for this task, call `edit_plan` instead. Do not return the plan only as prose, and do not claim completion until the plan tool succeeds. Once the plan is created, avoid asking follow-up questions unless you are genuinely blocked or materially uncertain about the next step.\n\n",
|
||||||
),
|
),
|
||||||
RigRequestMode::Orchestrate => prompt.push_str(
|
RigRequestMode::Orchestrate => prompt.push_str(
|
||||||
"## Orchestration Mode\nDelegate only independent, bounded work where parallelism materially helps, then synthesize the results.\n\n",
|
"## Orchestration Mode\nDelegate only independent, bounded work where parallelism materially helps, then synthesize the results.\n\n",
|
||||||
@@ -963,7 +964,7 @@ fn build_system_prompt(
|
|||||||
}
|
}
|
||||||
if tools.iter().any(|tool| tool.name == "create_plan") {
|
if tools.iter().any(|tool| tool.name == "create_plan") {
|
||||||
prompt.push_str(
|
prompt.push_str(
|
||||||
"Plan document creation is available through `create_plan`. When the user asks you to create a plan for review, research first as needed, then call `create_plan`; do not merely return the plan as prose or claim that no plan-creation tool is available. If the user asks to review the plan before implementation, creating the document and presenting it for review is the requested outcome; do not implement it until they approve.\n",
|
"Plan document creation is available through `create_plan`. When the user asks you to create a plan for review, research first as needed, then call `create_plan`; do not merely return the plan as prose or claim that no plan-creation tool is available. Include a concise `## Tasks` section with short, specific `- [ ]` items so the user can track progress. If the plan structure changes materially, invalidate it and create a replacement; status changes should update the existing task list instead. If the user asks to review the plan before implementation, creating the document and presenting it for review is the requested outcome; do not implement it until they approve. Once a plan and task list exist, continue working against them and avoid follow-up questions unless genuinely blocked or materially uncertain.\n",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user