Skip to content

Commit f566129

Browse files
committed
Refactor plan validation logic to prefer more complete candidates and enhance related tests
1 parent cc191c1 commit f566129

File tree

2 files changed

+139
-9
lines changed

2 files changed

+139
-9
lines changed

src/agent/runloop/unified/turn/turn_processing/plan_mode.rs

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,16 @@ Return JSON only.",
220220
.and_then(|content| parse_interview_payload_from_text(&content))
221221
.and_then(|payload| sanitize_generated_interview_payload(payload, &context));
222222

223-
let plan_validation = plan_context
224-
.as_ref()
225-
.and_then(|ctx| ctx.plan_validation.clone());
223+
let response_plan_validation = response_text
224+
.and_then(|text| extract_proposed_plan(text).plan_text)
225+
.as_deref()
226+
.map(validate_plan_content);
227+
let plan_validation = select_best_plan_validation(
228+
plan_context
229+
.as_ref()
230+
.and_then(|ctx| ctx.plan_validation.as_ref()),
231+
response_plan_validation.as_ref(),
232+
);
226233
let tracker_summary = plan_context
227234
.as_ref()
228235
.and_then(|ctx| ctx.tracker_summary.clone());
@@ -320,10 +327,30 @@ fn collect_interview_research_context(
320327
}
321328
}
322329

330+
let extracted_plan = response_text
331+
.and_then(|text| extract_proposed_plan(text).plan_text)
332+
.filter(|text| !text.trim().is_empty());
333+
let extracted_plan_excerpt = extracted_plan
334+
.as_deref()
335+
.map(|content| truncate_for_context(content, MAX_PLAN_DRAFT_CHARS));
336+
let extracted_plan_validation = extracted_plan.as_deref().map(validate_plan_content);
337+
let preferred_validation = select_best_plan_validation(
338+
plan_context.and_then(|ctx| ctx.plan_validation.as_ref()),
339+
extracted_plan_validation.as_ref(),
340+
);
341+
let prefer_extracted_plan = match (
342+
plan_context.and_then(|ctx| ctx.plan_validation.as_ref()),
343+
extracted_plan_validation.as_ref(),
344+
) {
345+
(Some(existing), Some(candidate)) => is_validation_better(candidate, existing),
346+
(None, Some(_)) => true,
347+
_ => false,
348+
};
349+
323350
let mut open_decision_hints = response_text
324351
.map(extract_open_decision_hints)
325352
.unwrap_or_default();
326-
if let Some(plan_validation) = plan_context.and_then(|ctx| ctx.plan_validation.as_ref()) {
353+
if let Some(plan_validation) = preferred_validation.as_ref() {
327354
for decision in plan_validation
328355
.open_decisions
329356
.iter()
@@ -333,21 +360,45 @@ fn collect_interview_research_context(
333360
}
334361
}
335362

363+
let extracted_plan_snapshot = extracted_plan_validation
364+
.as_ref()
365+
.map(PlanValidationSnapshot::from);
366+
336367
let (plan_draft_excerpt, plan_draft_path, plan_validation, task_tracker_excerpt,
337368
task_tracker_path, task_tracker_summary) = if let Some(plan_context) = plan_context {
369+
let plan_excerpt = if prefer_extracted_plan {
370+
extracted_plan_excerpt
371+
.clone()
372+
.or_else(|| plan_context.plan_excerpt.clone())
373+
} else {
374+
plan_context
375+
.plan_excerpt
376+
.clone()
377+
.or_else(|| extracted_plan_excerpt.clone())
378+
};
338379
(
339-
plan_context.plan_excerpt.clone(),
380+
plan_excerpt,
340381
plan_context.plan_path.clone(),
341-
plan_context
342-
.plan_validation
382+
preferred_validation
343383
.as_ref()
344-
.map(PlanValidationSnapshot::from),
384+
.map(PlanValidationSnapshot::from)
385+
.or(extracted_plan_snapshot.clone()),
345386
plan_context.tracker_excerpt.clone(),
346387
plan_context.tracker_path.clone(),
347388
plan_context.tracker_summary.clone(),
348389
)
349390
} else {
350-
(None, None, None, None, None, None)
391+
(
392+
extracted_plan_excerpt,
393+
None,
394+
preferred_validation
395+
.as_ref()
396+
.map(PlanValidationSnapshot::from)
397+
.or(extracted_plan_snapshot),
398+
None,
399+
None,
400+
None,
401+
)
351402
};
352403

353404
InterviewResearchContext {
@@ -1216,6 +1267,68 @@ fn extract_plan_validation(response_text: Option<&str>) -> Option<PlanValidation
12161267
Some(validate_plan_content(&plan_text))
12171268
}
12181269

1270+
fn select_best_plan_validation(
1271+
current: Option<&PlanValidationReport>,
1272+
candidate: Option<&PlanValidationReport>,
1273+
) -> Option<PlanValidationReport> {
1274+
match (current, candidate) {
1275+
(None, None) => None,
1276+
(Some(current), None) => Some(current.clone()),
1277+
(None, Some(candidate)) => Some(candidate.clone()),
1278+
(Some(current), Some(candidate)) => {
1279+
if is_validation_better(candidate, current) {
1280+
Some(candidate.clone())
1281+
} else {
1282+
Some(current.clone())
1283+
}
1284+
}
1285+
}
1286+
}
1287+
1288+
fn is_validation_better(
1289+
candidate: &PlanValidationReport,
1290+
current: &PlanValidationReport,
1291+
) -> bool {
1292+
if candidate.is_ready() && !current.is_ready() {
1293+
return true;
1294+
}
1295+
if current.is_ready() && !candidate.is_ready() {
1296+
return false;
1297+
}
1298+
1299+
let candidate_missing = candidate.missing_sections.len();
1300+
let current_missing = current.missing_sections.len();
1301+
if candidate_missing != current_missing {
1302+
return candidate_missing < current_missing;
1303+
}
1304+
1305+
let candidate_placeholders = candidate.placeholder_tokens.len();
1306+
let current_placeholders = current.placeholder_tokens.len();
1307+
if candidate_placeholders != current_placeholders {
1308+
return candidate_placeholders < current_placeholders;
1309+
}
1310+
1311+
let candidate_open = candidate.open_decisions.len();
1312+
let current_open = current.open_decisions.len();
1313+
if candidate_open != current_open {
1314+
return candidate_open < current_open;
1315+
}
1316+
1317+
if candidate.summary_present != current.summary_present {
1318+
return candidate.summary_present;
1319+
}
1320+
1321+
if candidate.implementation_step_count != current.implementation_step_count {
1322+
return candidate.implementation_step_count > current.implementation_step_count;
1323+
}
1324+
1325+
if candidate.validation_item_count != current.validation_item_count {
1326+
return candidate.validation_item_count > current.validation_item_count;
1327+
}
1328+
1329+
candidate.assumption_count > current.assumption_count
1330+
}
1331+
12191332
fn build_fallback_question(
12201333
id: &str,
12211334
header: &str,

src/agent/runloop/unified/turn/turn_processing/plan_mode/tests.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,20 @@ fn adaptive_fallback_interview_requests_task_metadata_when_missing() {
676676
let question_text = questions[0]["question"].as_str().unwrap_or("");
677677
assert!(question_text.to_ascii_lowercase().contains("task tracker"));
678678
}
679+
680+
#[test]
681+
fn select_best_plan_validation_prefers_more_complete_candidate() {
682+
let mut current = PlanValidationReport::default();
683+
current.missing_sections.push("Summary".to_string());
684+
current.placeholder_tokens.push("[step]".to_string());
685+
686+
let mut candidate = PlanValidationReport::default();
687+
candidate.summary_present = true;
688+
candidate.implementation_step_count = 2;
689+
candidate.validation_item_count = 1;
690+
candidate.assumption_count = 1;
691+
692+
let selected = select_best_plan_validation(Some(&current), Some(&candidate))
693+
.expect("expected selected validation");
694+
assert_eq!(selected, candidate);
695+
}

0 commit comments

Comments
 (0)