Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Kept Auto execution direct when structured pre-analysis is unavailable, and
replaced fabricated numbered fallback tasks with one step containing the
original request when planning is explicitly enabled.

## [6.4.2] - 2026-07-23

### Changed
Expand Down
1 change: 0 additions & 1 deletion core/prompts/planning/plan_fallback_step.md

This file was deleted.

6 changes: 3 additions & 3 deletions core/src/agent/execution_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,17 @@ impl AgentLoop {
style
}

fn resolve_planning_decision(
pub(super) fn resolve_planning_decision(
&self,
style: AgentStyle,
_style: AgentStyle,
pre_analysis: Option<&PreAnalysis>,
) -> bool {
match self.config.planning_mode {
PlanningMode::Disabled => false,
PlanningMode::Enabled => true,
PlanningMode::Auto => pre_analysis
.map(|analysis| analysis.requires_planning)
.unwrap_or_else(|| style.requires_planning()),
.unwrap_or(false),
}
}

Expand Down
17 changes: 17 additions & 0 deletions core/src/agent/extra_agent_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,23 @@ fn test_disabled_planning_never_runs_pre_analysis() {
assert!(!agent.should_run_pre_analysis());
}

#[test]
fn auto_mode_does_not_fabricate_a_plan_when_pre_analysis_is_unavailable() {
let mock_client = Arc::new(MockLlmClient::new(vec![]));
let tool_executor = Arc::new(ToolExecutor::new("/tmp".to_string()));
let agent = AgentLoop::new(
mock_client,
tool_executor,
test_tool_context(),
AgentConfig::default(),
);

assert!(
!agent.resolve_planning_decision(crate::prompts::AgentStyle::Plan, None),
"Auto must fall back to direct execution when structured pre-analysis failed"
);
}

#[derive(Debug)]
struct PlanningHookRecorder {
events: Arc<std::sync::Mutex<Vec<crate::hooks::HookEvent>>>,
Expand Down
55 changes: 23 additions & 32 deletions core/src/planning/llm_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,37 +152,18 @@ impl LlmPlanner {
Self::achievement_from_value(result.object)
}

/// Create a fallback plan using heuristic logic (no LLM required)
/// Create a minimal fallback plan when explicit planning cannot use the LLM.
///
/// The original request is the only honest executable step available here.
/// Fabricating numbered placeholder steps makes the task tracker look active
/// without conveying useful progress.
pub fn fallback_plan(prompt: &str) -> ExecutionPlan {
let complexity = if prompt.len() < 50 {
Complexity::Simple
} else if prompt.len() < 150 {
Complexity::Medium
} else if prompt.len() < 300 {
Complexity::Complex
} else {
Complexity::VeryComplex
};

let mut plan = ExecutionPlan::new(prompt, complexity);

let step_count = match complexity {
Complexity::Simple => 2,
Complexity::Medium => 4,
Complexity::Complex => 7,
Complexity::VeryComplex => 10,
let content = match prompt.trim() {
"" => "Complete the requested task",
prompt => prompt,
};

for i in 0..step_count {
let step = Task::new(
format!("step-{}", i + 1),
crate::prompts::render(
crate::prompts::PLAN_FALLBACK_STEP,
&[("step_num", &(i + 1).to_string())],
),
);
plan.add_step(step);
}
let mut plan = ExecutionPlan::new(content, Complexity::Simple);
plan.add_step(Task::new("step-1", content));

plan
}
Expand Down Expand Up @@ -579,13 +560,23 @@ mod tests {
let short_prompt = "Fix bug";
let plan = LlmPlanner::fallback_plan(short_prompt);
assert_eq!(plan.complexity, Complexity::Simple);
assert_eq!(plan.steps.len(), 2);
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.goal, short_prompt);
assert_eq!(plan.steps[0].content, short_prompt);

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";
let plan = LlmPlanner::fallback_plan(long_prompt);
assert_eq!(plan.complexity, Complexity::VeryComplex);
assert_eq!(plan.steps.len(), 10);
assert_eq!(plan.complexity, Complexity::Simple);
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].content, long_prompt);
assert!(
!plan.steps[0].content.contains("Execute step"),
"fallback plans must not expose placeholder task text"
);

let plan = LlmPlanner::fallback_plan(" ");
assert_eq!(plan.goal, "Complete the requested task");
assert_eq!(plan.steps[0].content, "Complete the requested task");
}

#[test]
Expand Down
3 changes: 0 additions & 3 deletions core/src/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execu
/// Template for per-step execution prompt
pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");

/// Template for fallback plan step description
pub const PLAN_FALLBACK_STEP: &str = include_str!("../prompts/planning/plan_fallback_step.md");

/// Skill catalog header injected before listing available skill names/descriptions.
pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");

Expand Down
1 change: 0 additions & 1 deletion core/src/prompts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ fn test_all_prompts_loaded() {
assert!(!SKILLS_CATALOG_HEADER.is_empty());
assert!(!PLAN_EXECUTE_GOAL.is_empty());
assert!(!PLAN_EXECUTE_STEP.is_empty());
assert!(!PLAN_FALLBACK_STEP.is_empty());
}

#[test]
Expand Down
Loading