Fix child startup readiness and ordered provider tools
Wait for shell bootstrap before dispatching child prompts, serialize provider preprocessing, and make permission callbacks idempotent. Restrict task lists to concrete multistep plans and stop inferring completion from tool activity. Update regression fixtures and resolve existing lint and test-layout failures. Verified formatting, both presubmit Clippy commands, and 354 targeted nextest tests.
This commit is contained in:
@@ -26,9 +26,6 @@ pub(crate) struct RuntimeResponseConfig {
|
||||
pub(crate) max_context_tokens: Option<u32>,
|
||||
pub(crate) capabilities: RuntimeCapabilities,
|
||||
pub(crate) empty_output_message: Option<String>,
|
||||
/// Todo items supplied by the existing task transcript, when one is available.
|
||||
#[serde(skip)]
|
||||
pub(crate) todo_items: Option<Vec<api::TodoItem>>,
|
||||
}
|
||||
|
||||
/// Converts the provider-neutral runtime lifecycle into Galaxy's existing
|
||||
@@ -54,19 +51,16 @@ pub(crate) struct RuntimeResponseTranslator {
|
||||
pub(crate) struct ProviderRunResponseProjector {
|
||||
translator: RuntimeResponseTranslator,
|
||||
has_started_model_turn: bool,
|
||||
todo_phase: usize,
|
||||
todo_started: bool,
|
||||
plan_tasks_projected: bool,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl ProviderRunResponseProjector {
|
||||
pub(crate) fn new(config: RuntimeResponseConfig) -> Self {
|
||||
let todo_started = config.todo_items.is_some();
|
||||
Self {
|
||||
translator: RuntimeResponseTranslator::new(config),
|
||||
has_started_model_turn: false,
|
||||
todo_phase: 0,
|
||||
todo_started,
|
||||
plan_tasks_projected: false,
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
@@ -78,9 +72,7 @@ impl ProviderRunResponseProjector {
|
||||
Self {
|
||||
translator: RuntimeResponseTranslator::restored(config, projection_was_initialized),
|
||||
has_started_model_turn: false,
|
||||
// Task-list events are part of the already persisted projection.
|
||||
todo_phase: usize::MAX,
|
||||
todo_started: true,
|
||||
plan_tasks_projected: true,
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
@@ -98,11 +90,9 @@ impl ProviderRunResponseProjector {
|
||||
self.translator.begin_followup_turn();
|
||||
}
|
||||
self.has_started_model_turn = true;
|
||||
let mut events = self.translator.translate(AgentEvent::TurnStarted {
|
||||
self.translator.translate(AgentEvent::TurnStarted {
|
||||
runtime_request_id: String::new(),
|
||||
})?;
|
||||
events.extend(self.todo_phase_events());
|
||||
Ok(events)
|
||||
})
|
||||
}
|
||||
ProviderRunProjection::ModelEvent { event, .. } => self.translator.translate(event),
|
||||
ProviderRunProjection::ModelRetry { .. } => {
|
||||
@@ -111,10 +101,9 @@ impl ProviderRunResponseProjector {
|
||||
ProviderRunProjection::ModelTurnRequested { .. }
|
||||
| ProviderRunProjection::ModelTurnFinished { .. } => Ok(Vec::new()),
|
||||
ProviderRunProjection::ToolBatchReady { batch } => {
|
||||
if !self.todo_started {
|
||||
if !self.plan_tasks_projected {
|
||||
if let Some(todos) = todos_from_plan_batch(&batch) {
|
||||
self.todo_started = true;
|
||||
self.todo_phase = 1;
|
||||
self.plan_tasks_projected = true;
|
||||
return Ok(vec![build_todo_update(
|
||||
&self.translator.config.task_id,
|
||||
api::message::update_todos::Operation::CreateTodoList(
|
||||
@@ -124,34 +113,9 @@ impl ProviderRunResponseProjector {
|
||||
),
|
||||
)]);
|
||||
}
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let todo_index = self.todo_phase.saturating_sub(1);
|
||||
let Some(todo) = self.todo_items().get(todo_index).cloned() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
self.todo_phase += 1;
|
||||
let mut events = vec![build_todo_update(
|
||||
&self.translator.config.task_id,
|
||||
api::message::update_todos::Operation::MarkTodosCompleted(
|
||||
api::MarkTodosCompleted {
|
||||
todo_ids: vec![todo.id],
|
||||
},
|
||||
),
|
||||
)];
|
||||
events.push(build_todo_update(
|
||||
&self.translator.config.task_id,
|
||||
api::message::update_todos::Operation::UpdatePendingTodos(
|
||||
api::UpdatePendingTodos {
|
||||
updated_pending_todos: self
|
||||
.todo_items()
|
||||
.into_iter()
|
||||
.skip(self.todo_phase)
|
||||
.collect(),
|
||||
},
|
||||
),
|
||||
));
|
||||
Ok(events)
|
||||
// Tool batches indicate runtime activity, not completion of planned work.
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,14 +134,9 @@ impl ProviderRunResponseProjector {
|
||||
}
|
||||
self.finished = true;
|
||||
match outcome {
|
||||
ProviderRunOutcome::Completed(completion) => {
|
||||
let mut events = self.todo_completion_events();
|
||||
events.extend(
|
||||
self.translator
|
||||
.finish_provider_run(completion.stop_reason.clone(), aggregate_usage),
|
||||
);
|
||||
Ok(events)
|
||||
}
|
||||
ProviderRunOutcome::Completed(completion) => Ok(self
|
||||
.translator
|
||||
.finish_provider_run(completion.stop_reason.clone(), aggregate_usage)),
|
||||
ProviderRunOutcome::Failed(failure) => Ok(self.translator.provider_failure(
|
||||
&failure.message,
|
||||
failure.source.as_ref(),
|
||||
@@ -188,49 +147,10 @@ impl ProviderRunResponseProjector {
|
||||
.finish_provider_run(StopReason::Cancelled, aggregate_usage)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
fn todo_phase_events(&mut self) -> Vec<ResponseEvent> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn todo_completion_events(&self) -> Vec<ResponseEvent> {
|
||||
if self.todo_phase == 0 || self.todo_phase == usize::MAX {
|
||||
return Vec::new();
|
||||
}
|
||||
let todos = self.todo_items();
|
||||
if todos.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
vec![
|
||||
build_todo_update(
|
||||
&self.translator.config.task_id,
|
||||
api::message::update_todos::Operation::MarkTodosCompleted(
|
||||
api::MarkTodosCompleted {
|
||||
todo_ids: todos.iter().map(|todo| todo.id.clone()).collect(),
|
||||
},
|
||||
),
|
||||
),
|
||||
build_todo_update(
|
||||
&self.translator.config.task_id,
|
||||
api::message::update_todos::Operation::UpdatePendingTodos(
|
||||
api::UpdatePendingTodos {
|
||||
updated_pending_todos: Vec::new(),
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
}
|
||||
fn todo_items(&self) -> Vec<api::TodoItem> {
|
||||
self.translator
|
||||
.config
|
||||
.todo_items
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// Only explicit multi-step checklists in a plan become tasks. Other document bullets
|
||||
// (risks, examples, requirements) are not executable tasks.
|
||||
fn todos_from_plan_batch(
|
||||
batch: &galaxy_agent_core::PendingToolBatch,
|
||||
) -> Option<Vec<api::TodoItem>> {
|
||||
@@ -240,46 +160,33 @@ fn todos_from_plan_batch(
|
||||
"create_plan" | "create_documents"
|
||||
)
|
||||
})?;
|
||||
let documents = plan_call.call.arguments.get("documents")?.as_array()?;
|
||||
let content = documents.first()?.get("content")?.as_str()?;
|
||||
let section = content
|
||||
.split_once("## Tasks")
|
||||
.or_else(|| content.split_once("## Implementation Tasks"))
|
||||
.map(|(_, section)| section)
|
||||
.unwrap_or(content);
|
||||
let todos = section
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let item = line
|
||||
.trim()
|
||||
.strip_prefix("- [ ]")
|
||||
.or_else(|| line.trim().strip_prefix("-"))?
|
||||
.trim();
|
||||
if item.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let title = item
|
||||
let content = plan_call
|
||||
.call
|
||||
.arguments
|
||||
.get("documents")?
|
||||
.as_array()?
|
||||
.first()?
|
||||
.get("content")?
|
||||
.as_str()?;
|
||||
let mut lines = content.lines();
|
||||
lines.find(|line| matches!(line.trim(), "## Tasks" | "## Implementation Tasks"))?;
|
||||
let todos = lines
|
||||
.take_while(|line| !line.trim().starts_with("## "))
|
||||
.filter_map(|line| line.trim().strip_prefix("- [ ]").map(str::trim))
|
||||
.filter(|item| !item.is_empty())
|
||||
.take(50)
|
||||
.enumerate()
|
||||
.map(|(index, item)| api::TodoItem {
|
||||
id: format!("plan-{}-{index}", plan_call.call.id),
|
||||
title: item
|
||||
.split_once(" - ")
|
||||
.map_or(item, |(title, _)| title)
|
||||
.trim();
|
||||
let id = format!(
|
||||
"plan-{}",
|
||||
title
|
||||
.chars()
|
||||
.filter_map(|character| character
|
||||
.is_ascii_alphanumeric()
|
||||
.then_some(character.to_ascii_lowercase()))
|
||||
.collect::<String>()
|
||||
);
|
||||
Some(api::TodoItem {
|
||||
id,
|
||||
title: title.to_owned(),
|
||||
description: item.to_owned(),
|
||||
})
|
||||
.trim()
|
||||
.to_owned(),
|
||||
description: item.to_owned(),
|
||||
})
|
||||
.take(50)
|
||||
.collect::<Vec<_>>();
|
||||
(!todos.is_empty()).then_some(todos)
|
||||
(todos.len() > 1).then_some(todos)
|
||||
}
|
||||
|
||||
fn build_todo_update(
|
||||
|
||||
@@ -17,7 +17,6 @@ fn provider_translator() -> RuntimeResponseTranslator {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ fn session_translator() -> RuntimeResponseTranslator {
|
||||
max_context_tokens: None,
|
||||
capabilities: RuntimeCapabilities::session_runtime(),
|
||||
empty_output_message: Some("> runtime completed without text".to_owned()),
|
||||
todo_items: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +44,6 @@ fn restored_provider_projection_skips_stream_initialization() {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
};
|
||||
let mut projector = ProviderRunResponseProjector::restored(config, true);
|
||||
let work_id = galaxy_agent_core::ExternalWorkId {
|
||||
@@ -86,24 +83,7 @@ fn restored_provider_projection_skips_stream_initialization() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_projection_uses_supplied_todos_and_advances_each_id() {
|
||||
let todos = vec![
|
||||
warp_multi_agent_api::TodoItem {
|
||||
id: "research".to_owned(),
|
||||
title: "Research".to_owned(),
|
||||
description: "Inspect".to_owned(),
|
||||
},
|
||||
warp_multi_agent_api::TodoItem {
|
||||
id: "implement".to_owned(),
|
||||
title: "Implement".to_owned(),
|
||||
description: "Edit".to_owned(),
|
||||
},
|
||||
warp_multi_agent_api::TodoItem {
|
||||
id: "verify".to_owned(),
|
||||
title: "Verify".to_owned(),
|
||||
description: "Check".to_owned(),
|
||||
},
|
||||
];
|
||||
fn provider_activity_does_not_create_or_complete_task_lists() {
|
||||
let mut projector = ProviderRunResponseProjector::new(RuntimeResponseConfig {
|
||||
task_id: "task".to_owned(),
|
||||
conversation_id: "conversation".to_owned(),
|
||||
@@ -113,13 +93,12 @@ fn provider_projection_uses_supplied_todos_and_advances_each_id() {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: Some(todos),
|
||||
});
|
||||
let work_id = galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
epoch: galaxy_agent_core::RunEpoch::new(1),
|
||||
};
|
||||
let initial = projector
|
||||
let mut events = projector
|
||||
.project(ProviderRunProjection::ModelTurnStarted {
|
||||
work_id: work_id.clone(),
|
||||
profile: galaxy_agent_core::ProviderRequestProfile::new("base"),
|
||||
@@ -130,62 +109,40 @@ fn provider_projection_uses_supplied_todos_and_advances_each_id() {
|
||||
elapsed_ms: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(initial.len(), 2);
|
||||
let first = projector
|
||||
assert_eq!(events.len(), 1);
|
||||
let tool_events = projector
|
||||
.project(ProviderRunProjection::ToolBatchReady {
|
||||
batch: galaxy_agent_core::PendingToolBatch {
|
||||
work_id: galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
epoch: galaxy_agent_core::RunEpoch::new(1),
|
||||
},
|
||||
work_id,
|
||||
calls: Vec::new(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
let second = projector
|
||||
.project(ProviderRunProjection::ToolBatchReady {
|
||||
batch: galaxy_agent_core::PendingToolBatch {
|
||||
work_id: galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
epoch: galaxy_agent_core::RunEpoch::new(1),
|
||||
},
|
||||
calls: Vec::new(),
|
||||
},
|
||||
})
|
||||
.unwrap();
|
||||
let ids = |events: &[warp_multi_agent_api::ResponseEvent]| {
|
||||
events
|
||||
.iter()
|
||||
.flat_map(|event| match &event.r#type {
|
||||
Some(response_event::Type::ClientActions(actions)) => actions
|
||||
.actions
|
||||
.iter()
|
||||
.filter_map(|action| match &action.action {
|
||||
Some(client_action::Action::AddMessagesToTask(add)) => add
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|message| match &message.message {
|
||||
Some(message::Message::UpdateTodos(update)) => match update
|
||||
.operation
|
||||
.as_ref()
|
||||
{
|
||||
Some(message::update_todos::Operation::MarkTodosCompleted(
|
||||
mark,
|
||||
)) => Some(mark.todo_ids[0].clone()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.next(),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
_ => Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(ids(&first), vec!["research"]);
|
||||
assert_eq!(ids(&second), vec!["implement"]);
|
||||
assert!(tool_events.is_empty());
|
||||
events.extend(
|
||||
projector
|
||||
.finish(
|
||||
&galaxy_agent_core::ProviderRunOutcome::Completed(
|
||||
galaxy_agent_core::ProviderRunCompletion {
|
||||
stop_reason: StopReason::Completed,
|
||||
},
|
||||
),
|
||||
&Usage::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
for event in events {
|
||||
if let Some(response_event::Type::ClientActions(actions)) = event.r#type {
|
||||
for action in actions.actions {
|
||||
if let Some(client_action::Action::AddMessagesToTask(add)) = action.action {
|
||||
assert!(add.messages.iter().all(|message| !matches!(
|
||||
message.message,
|
||||
Some(message::Message::UpdateTodos(_))
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -199,7 +156,6 @@ fn restored_uninitialized_projection_replays_init_before_live_delta() {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
};
|
||||
let mut projector = ProviderRunResponseProjector::restored(config, false);
|
||||
let work_id = galaxy_agent_core::ExternalWorkId {
|
||||
@@ -227,18 +183,11 @@ fn restored_uninitialized_projection_replays_init_before_live_delta() {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(started.len(), 2);
|
||||
assert_eq!(started.len(), 1);
|
||||
assert!(matches!(
|
||||
started[0].r#type,
|
||||
Some(response_event::Type::Init(_))
|
||||
));
|
||||
let Some(response_event::Type::ClientActions(actions)) = &started[1].r#type else {
|
||||
panic!("initial provider turn should publish its task list");
|
||||
};
|
||||
assert!(matches!(
|
||||
actions.actions[0].action,
|
||||
Some(client_action::Action::AddMessagesToTask(_))
|
||||
));
|
||||
assert_eq!(delta.len(), 1);
|
||||
assert!(matches!(
|
||||
delta[0].r#type,
|
||||
@@ -257,7 +206,6 @@ fn provider_followup_turn_starts_a_distinct_text_message() {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
});
|
||||
let first_work_id = galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
@@ -481,7 +429,6 @@ fn provider_retry_clears_failed_attempt_output_before_new_messages() {
|
||||
max_context_tokens: Some(1_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
});
|
||||
let work_id = galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
@@ -661,3 +608,35 @@ fn capabilities_reject_events_owned_by_the_other_runtime_shape() {
|
||||
})
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_tasks_require_multiple_explicit_checklist_items_in_a_task_section() {
|
||||
let batch = |content: &str| galaxy_agent_core::PendingToolBatch {
|
||||
work_id: galaxy_agent_core::ExternalWorkId {
|
||||
run_id: galaxy_agent_core::ProviderRunId::new("run"),
|
||||
epoch: galaxy_agent_core::RunEpoch::new(1),
|
||||
},
|
||||
calls: vec![galaxy_agent_core::PendingToolCall {
|
||||
call: galaxy_agent_core::ToolCall {
|
||||
id: "plan-call".to_string(),
|
||||
name: "create_plan".to_string(),
|
||||
arguments: serde_json::json!({"documents": [{"content": content}]}),
|
||||
},
|
||||
state: galaxy_agent_core::PendingToolCallState::Proposed,
|
||||
}],
|
||||
};
|
||||
for content in [
|
||||
"## Risks\n- [ ] Risk one\n- [ ] Risk two",
|
||||
"## Tasks\n- [ ] Update the parser",
|
||||
"## Tasks\n- Requirement one\n- Requirement two",
|
||||
] {
|
||||
assert!(super::todos_from_plan_batch(&batch(content)).is_none());
|
||||
}
|
||||
let todos = super::todos_from_plan_batch(&batch(
|
||||
"## Tasks\n- [ ] Add retry correlation\n- [ ] Test stale callbacks\n## Risks\n- [ ] Slow shell startup",
|
||||
)).unwrap();
|
||||
assert_eq!(todos.len(), 2);
|
||||
assert_eq!(todos[0].title, "Add retry correlation");
|
||||
assert_eq!(todos[1].title, "Test stale callbacks");
|
||||
assert_ne!(todos[0].id, todos[1].id);
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ async fn execution_failure_commits_one_correlated_error_result() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn inline_tool_batches_continue_without_leaving_the_coordinator() {
|
||||
async fn inline_tool_batches_commit_before_the_next_model_boundary() {
|
||||
let recall_turn = Ok(vec![
|
||||
started("request-recall"),
|
||||
Ok(AgentEvent::Tool {
|
||||
@@ -759,6 +759,13 @@ async fn inline_tool_batches_continue_without_leaving_the_coordinator() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(block, ProviderRunBlock::ReadyToCallModel));
|
||||
assert_eq!(runtime.requests().len(), 1);
|
||||
let (_sender, control) = turn_control();
|
||||
let block = coordinator
|
||||
.drive_until_blocked(control, collect_projection(&mut projections))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(block, ProviderRunBlock::AwaitingDriver { .. }));
|
||||
assert_eq!(runtime.requests().len(), 2);
|
||||
assert!(projections.iter().any(|projection| matches!(
|
||||
@@ -1208,7 +1215,6 @@ async fn transcript_projector_emits_one_ui_stream_for_the_whole_run() {
|
||||
max_context_tokens: Some(100_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
});
|
||||
let mut ui_events = Vec::new();
|
||||
let (_sender, control) = turn_control();
|
||||
@@ -1267,7 +1273,6 @@ fn transcript_projector_preserves_provider_failure_message() {
|
||||
max_context_tokens: Some(100_000),
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
empty_output_message: None,
|
||||
todo_items: None,
|
||||
});
|
||||
let events = projector
|
||||
.finish(
|
||||
|
||||
@@ -137,7 +137,6 @@ pub(crate) async fn prepare_provider_run(
|
||||
task_id,
|
||||
needs_create_task,
|
||||
user_query,
|
||||
todo_items,
|
||||
request,
|
||||
persistent_messages,
|
||||
tool_result_archive,
|
||||
@@ -160,7 +159,6 @@ pub(crate) async fn prepare_provider_run(
|
||||
max_context_tokens,
|
||||
capabilities: base_runtime.descriptor().capabilities.clone(),
|
||||
empty_output_message: None,
|
||||
todo_items,
|
||||
};
|
||||
Ok(PreparedProviderRun {
|
||||
base_profile: ProviderRunProfile::new(base_runtime, request),
|
||||
|
||||
@@ -27,7 +27,6 @@ pub(crate) struct PreparedRigTurn {
|
||||
pub task_id: String,
|
||||
pub needs_create_task: bool,
|
||||
pub user_query: Option<String>,
|
||||
pub todo_items: Option<Vec<api::TodoItem>>,
|
||||
pub request: TurnRequest,
|
||||
pub persistent_messages: Vec<ConversationMessage>,
|
||||
pub tool_result_archive: Vec<ConversationMessage>,
|
||||
@@ -234,9 +233,6 @@ fn prepare_rig_turn_for_provider(
|
||||
let needs_create_task = tasks.is_empty();
|
||||
let user_query = input.iter().find_map(input_user_query);
|
||||
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 {
|
||||
RigRequestMode::Cli => supported_cli_agent_tools,
|
||||
RigRequestMode::CompletedCommandAssessment => Vec::new(),
|
||||
@@ -304,7 +300,6 @@ fn prepare_rig_turn_for_provider(
|
||||
task_id,
|
||||
needs_create_task,
|
||||
user_query,
|
||||
todo_items,
|
||||
request,
|
||||
persistent_messages,
|
||||
tool_result_archive,
|
||||
@@ -313,29 +308,6 @@ fn prepare_rig_turn_for_provider(
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse a plan emitted by the model when the existing transcript contains one. This keeps
|
||||
// direct-provider projection aligned with UpdateTodos instead of inventing a second plan.
|
||||
fn todo_items_from_tasks(tasks: &[api::Task]) -> Option<Vec<api::TodoItem>> {
|
||||
let mut items = None;
|
||||
for task in tasks {
|
||||
for message in &task.messages {
|
||||
let Some(api::message::Message::UpdateTodos(update)) = &message.message else {
|
||||
continue;
|
||||
};
|
||||
match update.operation.as_ref()? {
|
||||
api::message::update_todos::Operation::CreateTodoList(create) => {
|
||||
items = Some(create.initial_todos.clone());
|
||||
}
|
||||
api::message::update_todos::Operation::UpdatePendingTodos(update) => {
|
||||
items = Some(update.updated_pending_todos.clone());
|
||||
}
|
||||
api::message::update_todos::Operation::MarkTodosCompleted(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
items.filter(|items| !items.is_empty())
|
||||
}
|
||||
|
||||
fn input_messages(
|
||||
inputs: Vec<AIAgentInput>,
|
||||
tool_results: Vec<ToolResult>,
|
||||
@@ -759,6 +731,9 @@ fn build_system_prompt(
|
||||
prompt.push_str(
|
||||
"## Execution Contract\nContinue until the user's requested outcome is complete and validated. Do not stop at an intermediate analysis, plan, status update, or promise of future work, and do not ask the user to say \"continue\". After each tool result, choose and perform the next necessary step. Stop only when the request is fulfilled or a concrete blocker requires user input; identify that blocker explicitly.\n\n",
|
||||
);
|
||||
prompt.push_str(
|
||||
"## Task Tracking\nUse task tracking only for substantial work with multiple concrete steps, after planning has established those steps or when the user provides an actionable plan. Skip it for simple requests, questions, and exploratory planning. Each task must name a specific change or deliverable with a clear completion condition. Do not create generic workflow items such as 'research the request', 'critique the plan', 'execute the plan', or 'verify the result'. Update tasks only when their actual work is complete; a tool call or model turn alone is not evidence of completion.\n\n",
|
||||
);
|
||||
let contexts = inputs.iter().filter_map(AIAgentInput::context).flatten();
|
||||
let mut environment = Vec::new();
|
||||
let mut request_time = None;
|
||||
@@ -921,7 +896,7 @@ fn build_system_prompt(
|
||||
match mode {
|
||||
RigRequestMode::Normal => {}
|
||||
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. 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",
|
||||
"## 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. For multi-step implementation work, 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(
|
||||
"## Orchestration Mode\nDelegate only independent, bounded work where parallelism materially helps, then synthesize the results.\n\n",
|
||||
@@ -964,7 +939,7 @@ fn build_system_prompt(
|
||||
}
|
||||
if tools.iter().any(|tool| tool.name == "create_plan") {
|
||||
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. 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",
|
||||
"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. For multi-step implementation work, 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,7 +517,9 @@ fn lrc_snapshot_follow_up_uses_the_cli_monitor_prompt_and_tools() {
|
||||
let prompt = prepared.request.system_prompt.expect("system prompt");
|
||||
|
||||
assert!(prompt.contains("## Running Command Monitor"));
|
||||
assert!(prompt.contains("`read_shell_command_output` with a short delay"));
|
||||
assert!(
|
||||
prompt.contains("Use the command ID from the tool result for every read/write operation")
|
||||
);
|
||||
assert!(prompt.contains("one concise, user-facing sentence"));
|
||||
assert!(prompt.contains("Do not send a text-only progress response"));
|
||||
assert!(prompt.contains("alternate screen containing `(END)` is `less`"));
|
||||
|
||||
Reference in New Issue
Block a user