Skip to content

Commit f33da92

Browse files
authored
Merge pull request #29 from A3S-Lab/fix/no-fabricated-plan-fallback-20260725
fix(agent): avoid fabricated fallback plans
2 parents 4b506da + 50d404c commit f33da92

7 files changed

Lines changed: 49 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Kept Auto execution direct when structured pre-analysis is unavailable, and
13+
replaced fabricated numbered fallback tasks with one step containing the
14+
original request when planning is explicitly enabled.
15+
1016
## [6.4.2] - 2026-07-23
1117

1218
### Changed

core/prompts/planning/plan_fallback_step.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

core/src/agent/execution_mode.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,17 +227,17 @@ impl AgentLoop {
227227
style
228228
}
229229

230-
fn resolve_planning_decision(
230+
pub(super) fn resolve_planning_decision(
231231
&self,
232-
style: AgentStyle,
232+
_style: AgentStyle,
233233
pre_analysis: Option<&PreAnalysis>,
234234
) -> bool {
235235
match self.config.planning_mode {
236236
PlanningMode::Disabled => false,
237237
PlanningMode::Enabled => true,
238238
PlanningMode::Auto => pre_analysis
239239
.map(|analysis| analysis.requires_planning)
240-
.unwrap_or_else(|| style.requires_planning()),
240+
.unwrap_or(false),
241241
}
242242
}
243243

core/src/agent/extra_agent_tests.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,23 @@ fn test_disabled_planning_never_runs_pre_analysis() {
610610
assert!(!agent.should_run_pre_analysis());
611611
}
612612

613+
#[test]
614+
fn auto_mode_does_not_fabricate_a_plan_when_pre_analysis_is_unavailable() {
615+
let mock_client = Arc::new(MockLlmClient::new(vec![]));
616+
let tool_executor = Arc::new(ToolExecutor::new("/tmp".to_string()));
617+
let agent = AgentLoop::new(
618+
mock_client,
619+
tool_executor,
620+
test_tool_context(),
621+
AgentConfig::default(),
622+
);
623+
624+
assert!(
625+
!agent.resolve_planning_decision(crate::prompts::AgentStyle::Plan, None),
626+
"Auto must fall back to direct execution when structured pre-analysis failed"
627+
);
628+
}
629+
613630
#[derive(Debug)]
614631
struct PlanningHookRecorder {
615632
events: Arc<std::sync::Mutex<Vec<crate::hooks::HookEvent>>>,

core/src/planning/llm_planner.rs

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -152,37 +152,18 @@ impl LlmPlanner {
152152
Self::achievement_from_value(result.object)
153153
}
154154

155-
/// Create a fallback plan using heuristic logic (no LLM required)
155+
/// Create a minimal fallback plan when explicit planning cannot use the LLM.
156+
///
157+
/// The original request is the only honest executable step available here.
158+
/// Fabricating numbered placeholder steps makes the task tracker look active
159+
/// without conveying useful progress.
156160
pub fn fallback_plan(prompt: &str) -> ExecutionPlan {
157-
let complexity = if prompt.len() < 50 {
158-
Complexity::Simple
159-
} else if prompt.len() < 150 {
160-
Complexity::Medium
161-
} else if prompt.len() < 300 {
162-
Complexity::Complex
163-
} else {
164-
Complexity::VeryComplex
165-
};
166-
167-
let mut plan = ExecutionPlan::new(prompt, complexity);
168-
169-
let step_count = match complexity {
170-
Complexity::Simple => 2,
171-
Complexity::Medium => 4,
172-
Complexity::Complex => 7,
173-
Complexity::VeryComplex => 10,
161+
let content = match prompt.trim() {
162+
"" => "Complete the requested task",
163+
prompt => prompt,
174164
};
175-
176-
for i in 0..step_count {
177-
let step = Task::new(
178-
format!("step-{}", i + 1),
179-
crate::prompts::render(
180-
crate::prompts::PLAN_FALLBACK_STEP,
181-
&[("step_num", &(i + 1).to_string())],
182-
),
183-
);
184-
plan.add_step(step);
185-
}
165+
let mut plan = ExecutionPlan::new(content, Complexity::Simple);
166+
plan.add_step(Task::new("step-1", content));
186167

187168
plan
188169
}
@@ -579,13 +560,23 @@ mod tests {
579560
let short_prompt = "Fix bug";
580561
let plan = LlmPlanner::fallback_plan(short_prompt);
581562
assert_eq!(plan.complexity, Complexity::Simple);
582-
assert_eq!(plan.steps.len(), 2);
563+
assert_eq!(plan.steps.len(), 1);
583564
assert_eq!(plan.goal, short_prompt);
565+
assert_eq!(plan.steps[0].content, short_prompt);
584566

585567
let long_prompt = "Implement a comprehensive authentication system with OAuth2 support, JWT tokens, refresh token rotation, multi-factor authentication, and role-based access control across all API endpoints with proper audit logging and session management capabilities for both web and mobile clients, including password reset flows, account lockout policies, and integration with external identity providers such as Google, GitHub, and SAML-based enterprise SSO systems";
586568
let plan = LlmPlanner::fallback_plan(long_prompt);
587-
assert_eq!(plan.complexity, Complexity::VeryComplex);
588-
assert_eq!(plan.steps.len(), 10);
569+
assert_eq!(plan.complexity, Complexity::Simple);
570+
assert_eq!(plan.steps.len(), 1);
571+
assert_eq!(plan.steps[0].content, long_prompt);
572+
assert!(
573+
!plan.steps[0].content.contains("Execute step"),
574+
"fallback plans must not expose placeholder task text"
575+
);
576+
577+
let plan = LlmPlanner::fallback_plan(" ");
578+
assert_eq!(plan.goal, "Complete the requested task");
579+
assert_eq!(plan.steps[0].content, "Complete the requested task");
589580
}
590581

591582
#[test]

core/src/prompts.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,6 @@ pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execu
8787
/// Template for per-step execution prompt
8888
pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");
8989

90-
/// Template for fallback plan step description
91-
pub const PLAN_FALLBACK_STEP: &str = include_str!("../prompts/planning/plan_fallback_step.md");
92-
9390
/// Skill catalog header injected before listing available skill names/descriptions.
9491
pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
9592

core/src/prompts/tests.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ fn test_all_prompts_loaded() {
1616
assert!(!SKILLS_CATALOG_HEADER.is_empty());
1717
assert!(!PLAN_EXECUTE_GOAL.is_empty());
1818
assert!(!PLAN_EXECUTE_STEP.is_empty());
19-
assert!(!PLAN_FALLBACK_STEP.is_empty());
2019
}
2120

2221
#[test]

0 commit comments

Comments
 (0)