🚀 Major Enhancement: Workflow-Based Tool Architecture v5.5.0 (#95)
* WIP: new workflow architecture * WIP: further improvements and cleanup * WIP: cleanup and docks, replace old tool with new * WIP: cleanup and docks, replace old tool with new * WIP: new planner implementation using workflow * WIP: precommit tool working as a workflow instead of a basic tool Support for passing False to use_assistant_model to skip external models completely and use Claude only * WIP: precommit workflow version swapped with old * WIP: codereview * WIP: replaced codereview * WIP: replaced codereview * WIP: replaced refactor * WIP: workflow for thinkdeep * WIP: ensure files get embedded correctly * WIP: thinkdeep replaced with workflow version * WIP: improved messaging when an external model's response is received * WIP: analyze tool swapped * WIP: updated tests * Extract only the content when building history * Use "relevant_files" for workflow tools only * WIP: updated tests * Extract only the content when building history * Use "relevant_files" for workflow tools only * WIP: fixed get_completion_next_steps_message missing param * Fixed tests Request for files consistently * Fixed tests Request for files consistently * Fixed tests * New testgen workflow tool Updated docs * Swap testgen workflow * Fix CI test failures by excluding API-dependent tests - Update GitHub Actions workflow to exclude simulation tests that require API keys - Fix collaboration tests to properly mock workflow tool expert analysis calls - Update test assertions to handle new workflow tool response format - Ensure unit tests run without external API dependencies in CI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * WIP - Update tests to match new tools * WIP - Update tests to match new tools --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
4dae6e457e
commit
69a3121452
800
tools/planner.py
800
tools/planner.py
@@ -1,80 +1,43 @@
|
||||
"""
|
||||
Planner tool
|
||||
Interactive Sequential Planner - Break down complex tasks through step-by-step planning
|
||||
|
||||
This tool helps you break down complex ideas, problems, or projects into multiple
|
||||
manageable steps. It enables Claude to think through larger problems sequentially, creating
|
||||
detailed action plans with clear dependencies and alternatives where applicable.
|
||||
This tool enables structured planning through an interactive, step-by-step process that builds
|
||||
plans incrementally with the ability to revise, branch, and adapt as understanding deepens.
|
||||
|
||||
=== CONTINUATION FLOW LOGIC ===
|
||||
The planner guides users through sequential thinking with forced pauses between steps to ensure
|
||||
thorough consideration of alternatives, dependencies, and strategic decisions before moving to
|
||||
tactical implementation details.
|
||||
|
||||
The tool implements sophisticated continuation logic that enables multi-session planning:
|
||||
Key features:
|
||||
- Sequential planning with full context awareness
|
||||
- Forced deep reflection for complex plans (≥5 steps) in early stages
|
||||
- Branching capabilities for exploring alternative approaches
|
||||
- Revision capabilities to update earlier decisions
|
||||
- Dynamic step count adjustment as plans evolve
|
||||
- Self-contained completion without external expert analysis
|
||||
|
||||
RULE 1: No continuation_id + step_number=1
|
||||
→ Creates NEW planning thread
|
||||
→ NO previous context loaded
|
||||
→ Returns continuation_id for future steps
|
||||
|
||||
RULE 2: continuation_id provided + step_number=1
|
||||
→ Loads PREVIOUS COMPLETE PLAN as context
|
||||
→ Starts NEW planning session with historical context
|
||||
→ Claude sees summary of previous completed plan
|
||||
|
||||
RULE 3: continuation_id provided + step_number>1
|
||||
→ NO previous context loaded (middle of current planning session)
|
||||
→ Continues current planning without historical interference
|
||||
|
||||
RULE 4: next_step_required=false (final step)
|
||||
→ Stores COMPLETE PLAN summary in conversation memory
|
||||
→ Returns continuation_id for future planning sessions
|
||||
|
||||
=== CONCRETE EXAMPLE ===
|
||||
|
||||
FIRST PLANNING SESSION (Feature A):
|
||||
Call 1: planner(step="Plan user authentication", step_number=1, total_steps=3, next_step_required=true)
|
||||
→ NEW thread created: "uuid-abc123"
|
||||
→ Response: {"step_number": 1, "continuation_id": "uuid-abc123"}
|
||||
|
||||
Call 2: planner(step="Design login flow", step_number=2, total_steps=3, next_step_required=true, continuation_id="uuid-abc123")
|
||||
→ Middle of current plan - NO context loading
|
||||
→ Response: {"step_number": 2, "continuation_id": "uuid-abc123"}
|
||||
|
||||
Call 3: planner(step="Security implementation", step_number=3, total_steps=3, next_step_required=FALSE, continuation_id="uuid-abc123")
|
||||
→ FINAL STEP: Stores "COMPLETE PLAN: Security implementation (3 steps completed)"
|
||||
→ Response: {"step_number": 3, "planning_complete": true, "continuation_id": "uuid-abc123"}
|
||||
|
||||
LATER PLANNING SESSION (Feature B):
|
||||
Call 1: planner(step="Plan dashboard system", step_number=1, total_steps=2, next_step_required=true, continuation_id="uuid-abc123")
|
||||
→ Loads previous complete plan as context
|
||||
→ Response includes: "=== PREVIOUS COMPLETE PLAN CONTEXT === Security implementation..."
|
||||
→ Claude sees previous work and can build upon it
|
||||
|
||||
Call 2: planner(step="Dashboard widgets", step_number=2, total_steps=2, next_step_required=FALSE, continuation_id="uuid-abc123")
|
||||
→ FINAL STEP: Stores new complete plan summary
|
||||
→ Both planning sessions now available for future continuations
|
||||
|
||||
This enables Claude to say: "Continue planning feature C using the authentication and dashboard work"
|
||||
and the tool will provide context from both previous completed planning sessions.
|
||||
Perfect for: complex project planning, system design with unknowns, migration strategies,
|
||||
architectural decisions, and breaking down large problems into manageable steps.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tools.models import ToolModelCategory
|
||||
|
||||
from config import TEMPERATURE_BALANCED
|
||||
from systemprompts import PLANNER_PROMPT
|
||||
from tools.shared.base_models import WorkflowRequest
|
||||
|
||||
from .base import BaseTool, ToolRequest
|
||||
from .workflow.base import WorkflowTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Field descriptions to avoid duplication between Pydantic and JSON schema
|
||||
# Tool-specific field descriptions matching original planner tool
|
||||
PLANNER_FIELD_DESCRIPTIONS = {
|
||||
# Interactive planning fields for step-by-step planning
|
||||
"step": (
|
||||
"Your current planning step. For the first step, describe the task/problem to plan and be extremely expressive "
|
||||
"so that subsequent steps can break this down into simpler steps. "
|
||||
@@ -91,25 +54,11 @@ PLANNER_FIELD_DESCRIPTIONS = {
|
||||
"branch_from_step": "If is_branch_point is true, which step number is the branching point",
|
||||
"branch_id": "Identifier for the current branch (e.g., 'approach-A', 'microservices-path')",
|
||||
"more_steps_needed": "True if more steps are needed beyond the initial estimate",
|
||||
"continuation_id": "Thread continuation ID for multi-turn planning sessions (useful for seeding new plans with prior context)",
|
||||
}
|
||||
|
||||
|
||||
class PlanStep:
|
||||
"""Represents a single step in the planning process."""
|
||||
|
||||
def __init__(
|
||||
self, step_number: int, content: str, branch_id: Optional[str] = None, parent_step: Optional[int] = None
|
||||
):
|
||||
self.step_number = step_number
|
||||
self.content = content
|
||||
self.branch_id = branch_id or "main"
|
||||
self.parent_step = parent_step
|
||||
self.children = []
|
||||
|
||||
|
||||
class PlannerRequest(ToolRequest):
|
||||
"""Request model for the planner tool - interactive step-by-step planning."""
|
||||
class PlannerRequest(WorkflowRequest):
|
||||
"""Request model for planner workflow tool matching original planner exactly"""
|
||||
|
||||
# Required fields for each planning step
|
||||
step: str = Field(..., description=PLANNER_FIELD_DESCRIPTIONS["step"])
|
||||
@@ -117,7 +66,7 @@ class PlannerRequest(ToolRequest):
|
||||
total_steps: int = Field(..., description=PLANNER_FIELD_DESCRIPTIONS["total_steps"])
|
||||
next_step_required: bool = Field(..., description=PLANNER_FIELD_DESCRIPTIONS["next_step_required"])
|
||||
|
||||
# Optional revision/branching fields
|
||||
# Optional revision/branching fields (planning-specific)
|
||||
is_step_revision: Optional[bool] = Field(False, description=PLANNER_FIELD_DESCRIPTIONS["is_step_revision"])
|
||||
revises_step_number: Optional[int] = Field(None, description=PLANNER_FIELD_DESCRIPTIONS["revises_step_number"])
|
||||
is_branch_point: Optional[bool] = Field(False, description=PLANNER_FIELD_DESCRIPTIONS["is_branch_point"])
|
||||
@@ -125,23 +74,58 @@ class PlannerRequest(ToolRequest):
|
||||
branch_id: Optional[str] = Field(None, description=PLANNER_FIELD_DESCRIPTIONS["branch_id"])
|
||||
more_steps_needed: Optional[bool] = Field(False, description=PLANNER_FIELD_DESCRIPTIONS["more_steps_needed"])
|
||||
|
||||
# Optional continuation field
|
||||
continuation_id: Optional[str] = Field(None, description=PLANNER_FIELD_DESCRIPTIONS["continuation_id"])
|
||||
# Exclude all investigation/analysis fields that aren't relevant to planning
|
||||
findings: str = Field(
|
||||
default="", exclude=True, description="Not used for planning - step content serves as findings"
|
||||
)
|
||||
files_checked: list[str] = Field(default_factory=list, exclude=True, description="Planning doesn't examine files")
|
||||
relevant_files: list[str] = Field(default_factory=list, exclude=True, description="Planning doesn't use files")
|
||||
relevant_context: list[str] = Field(
|
||||
default_factory=list, exclude=True, description="Planning doesn't track code context"
|
||||
)
|
||||
issues_found: list[dict] = Field(default_factory=list, exclude=True, description="Planning doesn't find issues")
|
||||
confidence: str = Field(default="planning", exclude=True, description="Planning uses different confidence model")
|
||||
hypothesis: Optional[str] = Field(default=None, exclude=True, description="Planning doesn't use hypothesis")
|
||||
backtrack_from_step: Optional[int] = Field(default=None, exclude=True, description="Planning uses revision instead")
|
||||
|
||||
# Override inherited fields to exclude them from schema
|
||||
model: Optional[str] = Field(default=None, exclude=True)
|
||||
# Exclude other non-planning fields
|
||||
temperature: Optional[float] = Field(default=None, exclude=True)
|
||||
thinking_mode: Optional[str] = Field(default=None, exclude=True)
|
||||
use_websearch: Optional[bool] = Field(default=None, exclude=True)
|
||||
images: Optional[list] = Field(default=None, exclude=True)
|
||||
use_assistant_model: Optional[bool] = Field(default=False, exclude=True, description="Planning is self-contained")
|
||||
images: Optional[list] = Field(default=None, exclude=True, description="Planning doesn't use images")
|
||||
|
||||
@field_validator("step_number")
|
||||
@classmethod
|
||||
def validate_step_number(cls, v):
|
||||
if v < 1:
|
||||
raise ValueError("step_number must be at least 1")
|
||||
return v
|
||||
|
||||
@field_validator("total_steps")
|
||||
@classmethod
|
||||
def validate_total_steps(cls, v):
|
||||
if v < 1:
|
||||
raise ValueError("total_steps must be at least 1")
|
||||
return v
|
||||
|
||||
|
||||
class PlannerTool(BaseTool):
|
||||
"""Sequential planning tool with step-by-step breakdown and refinement."""
|
||||
class PlannerTool(WorkflowTool):
|
||||
"""
|
||||
Planner workflow tool for step-by-step planning using the workflow architecture.
|
||||
|
||||
This tool provides the same planning capabilities as the original planner tool
|
||||
but uses the new workflow architecture for consistency with other workflow tools.
|
||||
It maintains all the original functionality including:
|
||||
- Sequential step-by-step planning
|
||||
- Branching and revision capabilities
|
||||
- Deep thinking pauses for complex plans
|
||||
- Conversation memory integration
|
||||
- Self-contained operation (no expert analysis)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.step_history = []
|
||||
self.branches = {}
|
||||
|
||||
def get_name(self) -> str:
|
||||
@@ -172,351 +156,381 @@ class PlannerTool(BaseTool):
|
||||
"migration strategies, architectural decisions, problem decomposition."
|
||||
)
|
||||
|
||||
def get_input_schema(self) -> dict[str, Any]:
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
# Interactive planning fields
|
||||
"step": {
|
||||
"type": "string",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["step"],
|
||||
},
|
||||
"step_number": {
|
||||
"type": "integer",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["step_number"],
|
||||
"minimum": 1,
|
||||
},
|
||||
"total_steps": {
|
||||
"type": "integer",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["total_steps"],
|
||||
"minimum": 1,
|
||||
},
|
||||
"next_step_required": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["next_step_required"],
|
||||
},
|
||||
"is_step_revision": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["is_step_revision"],
|
||||
},
|
||||
"revises_step_number": {
|
||||
"type": "integer",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["revises_step_number"],
|
||||
"minimum": 1,
|
||||
},
|
||||
"is_branch_point": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["is_branch_point"],
|
||||
},
|
||||
"branch_from_step": {
|
||||
"type": "integer",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["branch_from_step"],
|
||||
"minimum": 1,
|
||||
},
|
||||
"branch_id": {
|
||||
"type": "string",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["branch_id"],
|
||||
},
|
||||
"more_steps_needed": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["more_steps_needed"],
|
||||
},
|
||||
"continuation_id": {
|
||||
"type": "string",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["continuation_id"],
|
||||
},
|
||||
},
|
||||
# Required fields for interactive planning
|
||||
"required": ["step", "step_number", "total_steps", "next_step_required"],
|
||||
}
|
||||
return schema
|
||||
|
||||
def get_system_prompt(self) -> str:
|
||||
return PLANNER_PROMPT
|
||||
|
||||
def get_request_model(self):
|
||||
return PlannerRequest
|
||||
|
||||
def get_default_temperature(self) -> float:
|
||||
return TEMPERATURE_BALANCED
|
||||
|
||||
def get_model_category(self) -> "ToolModelCategory":
|
||||
"""Planner requires deep analysis and reasoning"""
|
||||
from tools.models import ToolModelCategory
|
||||
|
||||
return ToolModelCategory.EXTENDED_REASONING # Planning benefits from deep thinking
|
||||
|
||||
def get_default_thinking_mode(self) -> str:
|
||||
return "high" # Default to high thinking for comprehensive planning
|
||||
return ToolModelCategory.EXTENDED_REASONING
|
||||
|
||||
def requires_model(self) -> bool:
|
||||
"""
|
||||
Planner tool doesn't require AI model access - it's pure data processing.
|
||||
Planner tool doesn't require model resolution at the MCP boundary.
|
||||
|
||||
This prevents the server from trying to resolve model names like "auto"
|
||||
when the planner tool is used, since it overrides execute() and doesn't
|
||||
make any AI API calls.
|
||||
The planner is a pure data processing tool that organizes planning steps
|
||||
and provides structured guidance without calling external AI models.
|
||||
|
||||
Returns:
|
||||
bool: False - planner doesn't need AI model access
|
||||
"""
|
||||
return False
|
||||
|
||||
async def execute(self, arguments: dict[str, Any]) -> list:
|
||||
def get_workflow_request_model(self):
|
||||
"""Return the planner-specific request model."""
|
||||
return PlannerRequest
|
||||
|
||||
def get_tool_fields(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return planning-specific field definitions beyond the standard workflow fields."""
|
||||
return {
|
||||
# Planning-specific optional fields
|
||||
"is_step_revision": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["is_step_revision"],
|
||||
},
|
||||
"revises_step_number": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["revises_step_number"],
|
||||
},
|
||||
"is_branch_point": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["is_branch_point"],
|
||||
},
|
||||
"branch_from_step": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["branch_from_step"],
|
||||
},
|
||||
"branch_id": {
|
||||
"type": "string",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["branch_id"],
|
||||
},
|
||||
"more_steps_needed": {
|
||||
"type": "boolean",
|
||||
"description": PLANNER_FIELD_DESCRIPTIONS["more_steps_needed"],
|
||||
},
|
||||
}
|
||||
|
||||
def get_input_schema(self) -> dict[str, Any]:
|
||||
"""Generate input schema using WorkflowSchemaBuilder with field exclusion."""
|
||||
from .workflow.schema_builders import WorkflowSchemaBuilder
|
||||
|
||||
# Exclude investigation-specific fields that planning doesn't need
|
||||
excluded_workflow_fields = [
|
||||
"findings", # Planning uses step content instead
|
||||
"files_checked", # Planning doesn't examine files
|
||||
"relevant_files", # Planning doesn't use files
|
||||
"relevant_context", # Planning doesn't track code context
|
||||
"issues_found", # Planning doesn't find issues
|
||||
"confidence", # Planning uses different confidence model
|
||||
"hypothesis", # Planning doesn't use hypothesis
|
||||
"backtrack_from_step", # Planning uses revision instead
|
||||
]
|
||||
|
||||
# Exclude common fields that planning doesn't need
|
||||
excluded_common_fields = [
|
||||
"temperature", # Planning doesn't need temperature control
|
||||
"thinking_mode", # Planning doesn't need thinking mode
|
||||
"use_websearch", # Planning doesn't need web search
|
||||
"images", # Planning doesn't use images
|
||||
"files", # Planning doesn't use files
|
||||
]
|
||||
|
||||
return WorkflowSchemaBuilder.build_schema(
|
||||
tool_specific_fields=self.get_tool_fields(),
|
||||
required_fields=[], # No additional required fields beyond workflow defaults
|
||||
model_field_schema=self.get_model_field_schema(),
|
||||
auto_mode=self.is_effective_auto_mode(),
|
||||
tool_name=self.get_name(),
|
||||
excluded_workflow_fields=excluded_workflow_fields,
|
||||
excluded_common_fields=excluded_common_fields,
|
||||
)
|
||||
|
||||
# ================================================================================
|
||||
# Abstract Methods - Required Implementation from BaseWorkflowMixin
|
||||
# ================================================================================
|
||||
|
||||
def get_required_actions(self, step_number: int, confidence: str, findings: str, total_steps: int) -> list[str]:
|
||||
"""Define required actions for each planning phase."""
|
||||
if step_number == 1:
|
||||
# Initial planning tasks
|
||||
return [
|
||||
"Think deeply about the complete scope and complexity of what needs to be planned",
|
||||
"Consider multiple approaches and their trade-offs",
|
||||
"Identify key constraints, dependencies, and potential challenges",
|
||||
"Think about stakeholders, success criteria, and critical requirements",
|
||||
]
|
||||
elif step_number <= 3 and total_steps >= 5:
|
||||
# Complex plan early stages - force deep thinking
|
||||
if step_number == 2:
|
||||
return [
|
||||
"Evaluate the approach from step 1 - are there better alternatives?",
|
||||
"Break down the major phases and identify critical decision points",
|
||||
"Consider resource requirements and potential bottlenecks",
|
||||
"Think about how different parts interconnect and affect each other",
|
||||
]
|
||||
else: # step_number == 3
|
||||
return [
|
||||
"Validate that the emerging plan addresses the original requirements",
|
||||
"Identify any gaps or assumptions that need clarification",
|
||||
"Consider how to validate progress and adjust course if needed",
|
||||
"Think about what the first concrete steps should be",
|
||||
]
|
||||
else:
|
||||
# Later steps or simple plans
|
||||
return [
|
||||
"Continue developing the plan with concrete, actionable steps",
|
||||
"Consider implementation details and practical considerations",
|
||||
"Think about how to sequence and coordinate different activities",
|
||||
"Prepare for execution planning and resource allocation",
|
||||
]
|
||||
|
||||
def should_call_expert_analysis(self, consolidated_findings, request=None) -> bool:
|
||||
"""Planner is self-contained and doesn't need expert analysis."""
|
||||
return False
|
||||
|
||||
def prepare_expert_analysis_context(self, consolidated_findings) -> str:
|
||||
"""Planner doesn't use expert analysis."""
|
||||
return ""
|
||||
|
||||
def requires_expert_analysis(self) -> bool:
|
||||
"""Planner is self-contained like the original planner tool."""
|
||||
return False
|
||||
|
||||
# ================================================================================
|
||||
# Workflow Customization - Match Original Planner Behavior
|
||||
# ================================================================================
|
||||
|
||||
def prepare_step_data(self, request) -> dict:
|
||||
"""
|
||||
Override execute to work like original TypeScript tool - no AI calls, just data processing.
|
||||
|
||||
This method implements the core continuation logic that enables multi-session planning:
|
||||
|
||||
CONTINUATION LOGIC:
|
||||
1. If no continuation_id + step_number=1: Create new planning thread
|
||||
2. If continuation_id + step_number=1: Load previous complete plan as context for NEW planning
|
||||
3. If continuation_id + step_number>1: Continue current plan (no context loading)
|
||||
4. If next_step_required=false: Mark complete and store plan summary for future use
|
||||
|
||||
CONVERSATION MEMORY INTEGRATION:
|
||||
- Each step is stored in conversation memory for cross-tool continuation
|
||||
- Final steps store COMPLETE PLAN summaries that can be loaded as context
|
||||
- Only step 1 with continuation_id loads previous context (new planning session)
|
||||
- Steps 2+ with continuation_id continue current session without context interference
|
||||
Prepare step data from request with planner-specific fields.
|
||||
"""
|
||||
from mcp.types import TextContent
|
||||
step_data = {
|
||||
"step": request.step,
|
||||
"step_number": request.step_number,
|
||||
"findings": f"Planning step {request.step_number}: {request.step}", # Use step content as findings
|
||||
"files_checked": [], # Planner doesn't check files
|
||||
"relevant_files": [], # Planner doesn't use files
|
||||
"relevant_context": [], # Planner doesn't track context like debug
|
||||
"issues_found": [], # Planner doesn't track issues
|
||||
"confidence": "planning", # Planning confidence is different from investigation
|
||||
"hypothesis": None, # Planner doesn't use hypothesis
|
||||
"images": [], # Planner doesn't use images
|
||||
# Planner-specific fields
|
||||
"is_step_revision": request.is_step_revision or False,
|
||||
"revises_step_number": request.revises_step_number,
|
||||
"is_branch_point": request.is_branch_point or False,
|
||||
"branch_from_step": request.branch_from_step,
|
||||
"branch_id": request.branch_id,
|
||||
"more_steps_needed": request.more_steps_needed or False,
|
||||
}
|
||||
return step_data
|
||||
|
||||
from utils.conversation_memory import add_turn, create_thread, get_thread
|
||||
def build_base_response(self, request, continuation_id: str = None) -> dict:
|
||||
"""
|
||||
Build the base response structure with planner-specific fields.
|
||||
"""
|
||||
# Use work_history from workflow mixin for consistent step tracking
|
||||
# Add 1 to account for current step being processed
|
||||
current_step_count = len(self.work_history) + 1
|
||||
|
||||
try:
|
||||
# Validate request like the original
|
||||
request_model = self.get_request_model()
|
||||
request = request_model(**arguments)
|
||||
|
||||
# Process step like original TypeScript tool
|
||||
if request.step_number > request.total_steps:
|
||||
request.total_steps = request.step_number
|
||||
|
||||
# === CONTINUATION LOGIC IMPLEMENTATION ===
|
||||
# This implements the 4 rules documented in the module docstring
|
||||
|
||||
continuation_id = request.continuation_id
|
||||
previous_plan_context = ""
|
||||
|
||||
# RULE 1: No continuation_id + step_number=1 → Create NEW planning thread
|
||||
if not continuation_id and request.step_number == 1:
|
||||
# Filter arguments to only include serializable data for conversation memory
|
||||
serializable_args = {
|
||||
k: v
|
||||
for k, v in arguments.items()
|
||||
if not hasattr(v, "__class__") or v.__class__.__module__ != "utils.model_context"
|
||||
}
|
||||
continuation_id = create_thread("planner", serializable_args)
|
||||
# Result: New thread created, no previous context, returns continuation_id
|
||||
|
||||
# RULE 2: continuation_id + step_number=1 → Load PREVIOUS COMPLETE PLAN as context
|
||||
elif continuation_id and request.step_number == 1:
|
||||
thread = get_thread(continuation_id)
|
||||
if thread:
|
||||
# Search for most recent COMPLETE PLAN from previous planning sessions
|
||||
for turn in reversed(thread.turns): # Newest first
|
||||
if turn.tool_name == "planner" and turn.role == "assistant":
|
||||
# Try to parse as JSON first (new format)
|
||||
try:
|
||||
turn_data = json.loads(turn.content)
|
||||
if isinstance(turn_data, dict) and turn_data.get("planning_complete"):
|
||||
# New JSON format
|
||||
plan_summary = turn_data.get("plan_summary", "")
|
||||
if plan_summary:
|
||||
previous_plan_context = plan_summary[:500]
|
||||
break
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# Fallback to old text format
|
||||
if "planning_complete" in turn.content:
|
||||
try:
|
||||
if "COMPLETE PLAN:" in turn.content:
|
||||
plan_start = turn.content.find("COMPLETE PLAN:")
|
||||
previous_plan_context = turn.content[plan_start : plan_start + 500] + "..."
|
||||
else:
|
||||
previous_plan_context = turn.content[:300] + "..."
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if previous_plan_context:
|
||||
previous_plan_context = f"\\n\\n=== PREVIOUS COMPLETE PLAN CONTEXT ===\\n{previous_plan_context}\\n=== END CONTEXT ===\\n"
|
||||
# Result: NEW planning session with previous complete plan as context
|
||||
|
||||
# RULE 3: continuation_id + step_number>1 → Continue current plan (no context loading)
|
||||
# This case is handled by doing nothing - we're in the middle of current planning
|
||||
# Result: Current planning continues without historical interference
|
||||
|
||||
step_data = {
|
||||
"step": request.step,
|
||||
"step_number": request.step_number,
|
||||
"total_steps": request.total_steps,
|
||||
"next_step_required": request.next_step_required,
|
||||
"is_step_revision": request.is_step_revision,
|
||||
response_data = {
|
||||
"status": f"{self.get_name()}_in_progress",
|
||||
"step_number": request.step_number,
|
||||
"total_steps": request.total_steps,
|
||||
"next_step_required": request.next_step_required,
|
||||
"step_content": request.step,
|
||||
f"{self.get_name()}_status": {
|
||||
"files_checked": len(self.consolidated_findings.files_checked),
|
||||
"relevant_files": len(self.consolidated_findings.relevant_files),
|
||||
"relevant_context": len(self.consolidated_findings.relevant_context),
|
||||
"issues_found": len(self.consolidated_findings.issues_found),
|
||||
"images_collected": len(self.consolidated_findings.images),
|
||||
"current_confidence": self.get_request_confidence(request),
|
||||
"step_history_length": current_step_count, # Use work_history + current step
|
||||
},
|
||||
"metadata": {
|
||||
"branches": list(self.branches.keys()),
|
||||
"step_history_length": current_step_count, # Use work_history + current step
|
||||
"is_step_revision": request.is_step_revision or False,
|
||||
"revises_step_number": request.revises_step_number,
|
||||
"is_branch_point": request.is_branch_point,
|
||||
"is_branch_point": request.is_branch_point or False,
|
||||
"branch_from_step": request.branch_from_step,
|
||||
"branch_id": request.branch_id,
|
||||
"more_steps_needed": request.more_steps_needed,
|
||||
"continuation_id": request.continuation_id,
|
||||
}
|
||||
"more_steps_needed": request.more_steps_needed or False,
|
||||
},
|
||||
}
|
||||
|
||||
# Store in local history like original
|
||||
self.step_history.append(step_data)
|
||||
if continuation_id:
|
||||
response_data["continuation_id"] = continuation_id
|
||||
|
||||
# Handle branching like original
|
||||
if request.is_branch_point and request.branch_from_step and request.branch_id:
|
||||
if request.branch_id not in self.branches:
|
||||
self.branches[request.branch_id] = []
|
||||
self.branches[request.branch_id].append(step_data)
|
||||
return response_data
|
||||
|
||||
# Build structured JSON response like other tools (consensus, refactor)
|
||||
response_data = {
|
||||
"status": "planning_success",
|
||||
"step_number": request.step_number,
|
||||
"total_steps": request.total_steps,
|
||||
"next_step_required": request.next_step_required,
|
||||
"step_content": request.step,
|
||||
"metadata": {
|
||||
"branches": list(self.branches.keys()),
|
||||
"step_history_length": len(self.step_history),
|
||||
"is_step_revision": request.is_step_revision or False,
|
||||
"revises_step_number": request.revises_step_number,
|
||||
"is_branch_point": request.is_branch_point or False,
|
||||
"branch_from_step": request.branch_from_step,
|
||||
"branch_id": request.branch_id,
|
||||
"more_steps_needed": request.more_steps_needed or False,
|
||||
},
|
||||
"output": {
|
||||
"instructions": "This is a structured planning response. Present the step_content as the main planning analysis. If next_step_required is true, continue with the next step. If planning_complete is true, present the complete plan in a well-structured format with clear sections, headings, numbered steps, and visual elements like ASCII charts for phases/dependencies. Use bullet points, sub-steps, sequences, and visual organization to make complex plans easy to understand and follow. IMPORTANT: Do NOT use emojis - use clear text formatting and ASCII characters only. Do NOT mention time estimates or costs unless explicitly requested.",
|
||||
"format": "step_by_step_planning",
|
||||
"presentation_guidelines": {
|
||||
"completed_plans": "Use clear headings, numbered phases, ASCII diagrams for workflows/dependencies, bullet points for sub-tasks, and visual sequences where helpful. No emojis. No time/cost estimates unless requested.",
|
||||
"step_content": "Present as main analysis with clear structure and actionable insights. No emojis. No time/cost estimates unless requested.",
|
||||
"continuation": "Use continuation_id for related planning sessions or implementation planning",
|
||||
},
|
||||
},
|
||||
}
|
||||
def handle_work_continuation(self, response_data: dict, request) -> dict:
|
||||
"""
|
||||
Handle work continuation with planner-specific deep thinking pauses.
|
||||
"""
|
||||
response_data["status"] = f"pause_for_{self.get_name()}"
|
||||
response_data[f"{self.get_name()}_required"] = True
|
||||
|
||||
# Always include continuation_id if we have one (enables step chaining within session)
|
||||
if continuation_id:
|
||||
response_data["continuation_id"] = continuation_id
|
||||
# Get planner-specific required actions
|
||||
required_actions = self.get_required_actions(request.step_number, "planning", request.step, request.total_steps)
|
||||
response_data["required_actions"] = required_actions
|
||||
|
||||
# Add previous plan context if available
|
||||
if previous_plan_context:
|
||||
response_data["previous_plan_context"] = previous_plan_context.strip()
|
||||
# Enhanced deep thinking pauses for complex plans
|
||||
if request.total_steps >= 5 and request.step_number <= 3:
|
||||
response_data["status"] = "pause_for_deep_thinking"
|
||||
response_data["thinking_required"] = True
|
||||
response_data["required_thinking"] = required_actions
|
||||
|
||||
# RULE 4: next_step_required=false → Mark complete and store plan summary
|
||||
if not request.next_step_required:
|
||||
response_data["planning_complete"] = True
|
||||
response_data["plan_summary"] = (
|
||||
f"COMPLETE PLAN: {request.step} (Total {request.total_steps} steps completed)"
|
||||
)
|
||||
if request.step_number == 1:
|
||||
response_data["next_steps"] = (
|
||||
"Planning complete. Present the complete plan to the user in a well-structured format with clear sections, "
|
||||
"numbered steps, visual elements (ASCII charts/diagrams where helpful), sub-step breakdowns, and implementation guidance. "
|
||||
"Use headings, bullet points, and visual organization to make the plan easy to follow. "
|
||||
"If there are phases, dependencies, or parallel tracks, show these relationships visually. "
|
||||
"IMPORTANT: Do NOT use emojis - use clear text formatting and ASCII characters only. "
|
||||
"Do NOT mention time estimates or costs unless explicitly requested. "
|
||||
"After presenting the plan, offer to either help implement specific parts or use the continuation_id to start related planning sessions."
|
||||
f"MANDATORY: DO NOT call the {self.get_name()} tool again immediately. This is a complex plan ({request.total_steps} steps) "
|
||||
f"that requires deep thinking. You MUST first spend time reflecting on the planning challenge:\n\n"
|
||||
f"REQUIRED DEEP THINKING before calling {self.get_name()} step {request.step_number + 1}:\n"
|
||||
f"1. Analyze the FULL SCOPE: What exactly needs to be accomplished?\n"
|
||||
f"2. Consider MULTIPLE APPROACHES: What are 2-3 different ways to tackle this?\n"
|
||||
f"3. Identify CONSTRAINTS & DEPENDENCIES: What limits our options?\n"
|
||||
f"4. Think about SUCCESS CRITERIA: How will we know we've succeeded?\n"
|
||||
f"5. Consider RISKS & MITIGATION: What could go wrong early vs late?\n\n"
|
||||
f"Only call {self.get_name()} again with step_number: {request.step_number + 1} AFTER this deep analysis."
|
||||
)
|
||||
# Result: Planning marked complete, summary stored for future context loading
|
||||
else:
|
||||
response_data["planning_complete"] = False
|
||||
remaining_steps = request.total_steps - request.step_number
|
||||
|
||||
# ENHANCED: Add deep thinking pauses for complex plans in early stages
|
||||
# Only for complex plans (>=5 steps) and first 3 steps - force deep reflection
|
||||
if request.total_steps >= 5 and request.step_number <= 3:
|
||||
response_data["status"] = "pause_for_deep_thinking"
|
||||
response_data["thinking_required"] = True
|
||||
|
||||
if request.step_number == 1:
|
||||
# Initial deep thinking - understand the full scope
|
||||
response_data["required_thinking"] = [
|
||||
"Analyze the complete scope and complexity of what needs to be planned",
|
||||
"Consider multiple approaches and their trade-offs",
|
||||
"Identify key constraints, dependencies, and potential challenges",
|
||||
"Think about stakeholders, success criteria, and critical requirements",
|
||||
"Consider what could go wrong and how to mitigate risks early",
|
||||
]
|
||||
response_data["next_steps"] = (
|
||||
f"MANDATORY: DO NOT call the planner tool again immediately. This is a complex plan ({request.total_steps} steps) "
|
||||
f"that requires deep thinking. You MUST first spend time reflecting on the planning challenge:\n\n"
|
||||
f"REQUIRED DEEP THINKING before calling planner step {request.step_number + 1}:\n"
|
||||
f"1. Analyze the FULL SCOPE: What exactly needs to be accomplished?\n"
|
||||
f"2. Consider MULTIPLE APPROACHES: What are 2-3 different ways to tackle this?\n"
|
||||
f"3. Identify CONSTRAINTS & DEPENDENCIES: What limits our options?\n"
|
||||
f"4. Think about SUCCESS CRITERIA: How will we know we've succeeded?\n"
|
||||
f"5. Consider RISKS & MITIGATION: What could go wrong early vs late?\n\n"
|
||||
f"Only call planner again with step_number: {request.step_number + 1} AFTER this deep analysis."
|
||||
)
|
||||
elif request.step_number == 2:
|
||||
# Refine approach - dig deeper into the chosen direction
|
||||
response_data["required_thinking"] = [
|
||||
"Evaluate the approach from step 1 - are there better alternatives?",
|
||||
"Break down the major phases and identify critical decision points",
|
||||
"Consider resource requirements and potential bottlenecks",
|
||||
"Think about how different parts interconnect and affect each other",
|
||||
"Identify areas that need the most careful planning vs quick wins",
|
||||
]
|
||||
response_data["next_steps"] = (
|
||||
f"STOP! Complex planning requires reflection between steps. DO NOT call planner immediately.\n\n"
|
||||
f"MANDATORY REFLECTION before planner step {request.step_number + 1}:\n"
|
||||
f"1. EVALUATE YOUR APPROACH: Is the direction from step 1 still the best?\n"
|
||||
f"2. IDENTIFY MAJOR PHASES: What are the 3-5 main chunks of work?\n"
|
||||
f"3. SPOT DEPENDENCIES: What must happen before what?\n"
|
||||
f"4. CONSIDER RESOURCES: What skills, tools, or access do we need?\n"
|
||||
f"5. FIND CRITICAL PATHS: Where could delays hurt the most?\n\n"
|
||||
f"Think deeply about these aspects, then call planner with step_number: {request.step_number + 1}."
|
||||
)
|
||||
elif request.step_number == 3:
|
||||
# Final deep thinking - validate and prepare for execution planning
|
||||
response_data["required_thinking"] = [
|
||||
"Validate that the emerging plan addresses the original requirements",
|
||||
"Identify any gaps or assumptions that need clarification",
|
||||
"Consider how to validate progress and adjust course if needed",
|
||||
"Think about what the first concrete steps should be",
|
||||
"Prepare for transition from strategic to tactical planning",
|
||||
]
|
||||
response_data["next_steps"] = (
|
||||
f"PAUSE for final strategic reflection. DO NOT call planner yet.\n\n"
|
||||
f"FINAL DEEP THINKING before planner step {request.step_number + 1}:\n"
|
||||
f"1. VALIDATE COMPLETENESS: Does this plan address all original requirements?\n"
|
||||
f"2. CHECK FOR GAPS: What assumptions need validation? What's unclear?\n"
|
||||
f"3. PLAN FOR ADAPTATION: How will we know if we need to change course?\n"
|
||||
f"4. DEFINE FIRST STEPS: What are the first 2-3 concrete actions?\n"
|
||||
f"5. TRANSITION MINDSET: Ready to shift from strategic to tactical planning?\n\n"
|
||||
f"After this reflection, call planner with step_number: {request.step_number + 1} to continue with tactical details."
|
||||
)
|
||||
else:
|
||||
# Normal flow for simple plans or later steps of complex plans
|
||||
response_data["next_steps"] = (
|
||||
f"Continue with step {request.step_number + 1}. Approximately {remaining_steps} steps remaining."
|
||||
)
|
||||
# Result: Intermediate step, planning continues (with optional deep thinking pause)
|
||||
|
||||
# Convert to clean JSON response
|
||||
response_content = json.dumps(response_data, indent=2)
|
||||
|
||||
# Store this step in conversation memory
|
||||
if continuation_id:
|
||||
add_turn(
|
||||
thread_id=continuation_id,
|
||||
role="assistant",
|
||||
content=response_content,
|
||||
tool_name="planner",
|
||||
model_name="claude-planner",
|
||||
elif request.step_number == 2:
|
||||
response_data["next_steps"] = (
|
||||
f"STOP! Complex planning requires reflection between steps. DO NOT call {self.get_name()} immediately.\n\n"
|
||||
f"MANDATORY REFLECTION before {self.get_name()} step {request.step_number + 1}:\n"
|
||||
f"1. EVALUATE YOUR APPROACH: Is the direction from step 1 still the best?\n"
|
||||
f"2. IDENTIFY MAJOR PHASES: What are the 3-5 main chunks of work?\n"
|
||||
f"3. SPOT DEPENDENCIES: What must happen before what?\n"
|
||||
f"4. CONSIDER RESOURCES: What skills, tools, or access do we need?\n"
|
||||
f"5. FIND CRITICAL PATHS: Where could delays hurt the most?\n\n"
|
||||
f"Think deeply about these aspects, then call {self.get_name()} with step_number: {request.step_number + 1}."
|
||||
)
|
||||
elif request.step_number == 3:
|
||||
response_data["next_steps"] = (
|
||||
f"PAUSE for final strategic reflection. DO NOT call {self.get_name()} yet.\n\n"
|
||||
f"FINAL DEEP THINKING before {self.get_name()} step {request.step_number + 1}:\n"
|
||||
f"1. VALIDATE COMPLETENESS: Does this plan address all original requirements?\n"
|
||||
f"2. CHECK FOR GAPS: What assumptions need validation? What's unclear?\n"
|
||||
f"3. PLAN FOR ADAPTATION: How will we know if we need to change course?\n"
|
||||
f"4. DEFINE FIRST STEPS: What are the first 2-3 concrete actions?\n"
|
||||
f"5. TRANSITION MINDSET: Ready to shift from strategic to tactical planning?\n\n"
|
||||
f"After this reflection, call {self.get_name()} with step_number: {request.step_number + 1} to continue with tactical details."
|
||||
)
|
||||
else:
|
||||
# Normal flow for simple plans or later steps
|
||||
remaining_steps = request.total_steps - request.step_number
|
||||
response_data["next_steps"] = (
|
||||
f"Continue with step {request.step_number + 1}. Approximately {remaining_steps} steps remaining."
|
||||
)
|
||||
|
||||
# Return the JSON response directly as text content, like consensus tool
|
||||
return [TextContent(type="text", text=response_content)]
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
# Error handling - return JSON directly like consensus tool
|
||||
error_data = {"error": str(e), "status": "planning_failed"}
|
||||
return [TextContent(type="text", text=json.dumps(error_data, indent=2))]
|
||||
def customize_workflow_response(self, response_data: dict, request) -> dict:
|
||||
"""
|
||||
Customize response to match original planner tool format.
|
||||
"""
|
||||
# No need to append to step_history since workflow mixin already manages work_history
|
||||
# and we calculate step counts from work_history
|
||||
|
||||
# Stub implementations for abstract methods (not used since we override execute)
|
||||
async def prepare_prompt(self, request: PlannerRequest) -> str:
|
||||
return "" # Not used - execute() is overridden
|
||||
# Handle branching like original planner
|
||||
if request.is_branch_point and request.branch_from_step and request.branch_id:
|
||||
if request.branch_id not in self.branches:
|
||||
self.branches[request.branch_id] = []
|
||||
step_data = self.prepare_step_data(request)
|
||||
self.branches[request.branch_id].append(step_data)
|
||||
|
||||
def format_response(self, response: str, request: PlannerRequest, model_info: dict = None) -> str:
|
||||
return response # Not used - execute() is overridden
|
||||
# Update metadata to reflect the new branch
|
||||
if "metadata" in response_data:
|
||||
response_data["metadata"]["branches"] = list(self.branches.keys())
|
||||
|
||||
# Add planner-specific output instructions for final steps
|
||||
if not request.next_step_required:
|
||||
response_data["planning_complete"] = True
|
||||
response_data["plan_summary"] = (
|
||||
f"COMPLETE PLAN: {request.step} (Total {request.total_steps} steps completed)"
|
||||
)
|
||||
response_data["output"] = {
|
||||
"instructions": "This is a structured planning response. Present the step_content as the main planning analysis. If next_step_required is true, continue with the next step. If planning_complete is true, present the complete plan in a well-structured format with clear sections, headings, numbered steps, and visual elements like ASCII charts for phases/dependencies. Use bullet points, sub-steps, sequences, and visual organization to make complex plans easy to understand and follow. IMPORTANT: Do NOT use emojis - use clear text formatting and ASCII characters only. Do NOT mention time estimates or costs unless explicitly requested.",
|
||||
"format": "step_by_step_planning",
|
||||
"presentation_guidelines": {
|
||||
"completed_plans": "Use clear headings, numbered phases, ASCII diagrams for workflows/dependencies, bullet points for sub-tasks, and visual sequences where helpful. No emojis. No time/cost estimates unless requested.",
|
||||
"step_content": "Present as main analysis with clear structure and actionable insights. No emojis. No time/cost estimates unless requested.",
|
||||
"continuation": "Use continuation_id for related planning sessions or implementation planning",
|
||||
},
|
||||
}
|
||||
response_data["next_steps"] = (
|
||||
"Planning complete. Present the complete plan to the user in a well-structured format with clear sections, "
|
||||
"numbered steps, visual elements (ASCII charts/diagrams where helpful), sub-step breakdowns, and implementation guidance. "
|
||||
"Use headings, bullet points, and visual organization to make the plan easy to follow. "
|
||||
"If there are phases, dependencies, or parallel tracks, show these relationships visually. "
|
||||
"IMPORTANT: Do NOT use emojis - use clear text formatting and ASCII characters only. "
|
||||
"Do NOT mention time estimates or costs unless explicitly requested. "
|
||||
"After presenting the plan, offer to either help implement specific parts or use the continuation_id to start related planning sessions."
|
||||
)
|
||||
|
||||
# Convert generic status names to planner-specific ones
|
||||
tool_name = self.get_name()
|
||||
status_mapping = {
|
||||
f"{tool_name}_in_progress": "planning_success",
|
||||
f"pause_for_{tool_name}": f"pause_for_{tool_name}", # Keep the full tool name for workflow consistency
|
||||
f"{tool_name}_required": f"{tool_name}_required", # Keep the full tool name for workflow consistency
|
||||
f"{tool_name}_complete": f"{tool_name}_complete", # Keep the full tool name for workflow consistency
|
||||
}
|
||||
|
||||
if response_data["status"] in status_mapping:
|
||||
response_data["status"] = status_mapping[response_data["status"]]
|
||||
|
||||
return response_data
|
||||
|
||||
# ================================================================================
|
||||
# Hook Method Overrides for Planner-Specific Behavior
|
||||
# ================================================================================
|
||||
|
||||
def get_completion_status(self) -> str:
|
||||
"""Planner uses planning-specific status."""
|
||||
return "planning_complete"
|
||||
|
||||
def get_completion_data_key(self) -> str:
|
||||
"""Planner uses 'complete_planning' key."""
|
||||
return "complete_planning"
|
||||
|
||||
def get_completion_message(self) -> str:
|
||||
"""Planner-specific completion message."""
|
||||
return (
|
||||
"Planning complete. Present the complete plan to the user in a well-structured format "
|
||||
"and offer to help implement specific parts or start related planning sessions."
|
||||
)
|
||||
|
||||
def get_skip_reason(self) -> str:
|
||||
"""Planner-specific skip reason."""
|
||||
return "Planner is self-contained and completes planning without external analysis"
|
||||
|
||||
def get_skip_expert_analysis_status(self) -> str:
|
||||
"""Planner-specific expert analysis skip status."""
|
||||
return "skipped_by_tool_design"
|
||||
|
||||
def store_initial_issue(self, step_description: str):
|
||||
"""Store initial planning description."""
|
||||
self.initial_planning_description = step_description
|
||||
|
||||
def get_initial_request(self, fallback_step: str) -> str:
|
||||
"""Get initial planning description."""
|
||||
try:
|
||||
return self.initial_planning_description
|
||||
except AttributeError:
|
||||
return fallback_step
|
||||
|
||||
# Required abstract methods from BaseTool
|
||||
def get_request_model(self):
|
||||
"""Return the planner-specific request model."""
|
||||
return PlannerRequest
|
||||
|
||||
async def prepare_prompt(self, request) -> str:
|
||||
"""Not used - workflow tools use execute_workflow()."""
|
||||
return "" # Workflow tools use execute_workflow() directly
|
||||
|
||||
Reference in New Issue
Block a user