Skip to main content
The planning node transforms diagnostic insights into concrete, executable remediation plans. It generates shell commands tailored to the specific environment while enforcing safety constraints and learning from past failures.

Overview

Planning bridges the gap between understanding (diagnosis) and action (execution). The node uses an LLM to generate 1-3 shell commands that will remediate the detected issue.
The planner is context-aware of the target environment: an Ubuntu server accessed via SSH with a non-root user (sentinel), requiring sudo for administrative tasks.

LLM Configuration

llm = ChatOpenAI(model=config.MODEL_NAME, temperature=config.TEMPERATURE)
The planner uses the same LLM configuration as the diagnosis node for consistency.

Planning Workflow

1

State Extraction

Gathers context from the agent state:
diagnosis = state.get("diagnosis_log", [])[-1] if state.get("diagnosis_log") else "Sin diagnostico"
error = state.get("current_error", "desconocido")
service = state.get("affected_service", "desconocido")
retry_count = state.get("retry_count", 0)
2

Failed Commands Lookup

Retrieves commands that previously failed to avoid repetition:
failed_commands = memory.get_failed_commands(error)
failed_str = ""
if failed_commands:
    failed_str = "\nCOMANDOS QUE YA FALLARON (PROHIBIDO repetirlos):\n"
    for cmd in failed_commands:
        failed_str += f"- {cmd}\n"
3

Command Generation

Sends a structured prompt to the LLM requesting remediation commands:
messages = [
    SystemMessage(content=(
        "Eres un motor de automatizacion DevOps.\n"
        "Genera un plan de remediacion con 1 a 3 comandos de shell.\n\n"
        f"- Servicio afectado: {service}\n"
        f"{failed_str}"
    )),
    HumanMessage(content=(
        f"Error: {error}\n"
        f"Intento: {retry_count + 1}\n"
        f"Diagnostico: {diagnosis}"
    ))
]
4

Command Parsing

Cleans and validates the LLM response:
raw = response.content.strip().replace("`", "").replace("```", "")
commands = [line.strip() for line in raw.split("\n")
            if line.strip() and not line.strip().startswith("#")]
5

Sudo Injection

Automatically adds sudo to commands that require administrative privileges:
commands = [ensure_sudo(cmd) for cmd in commands]

Sudo Management

The ensure_sudo function automatically prepends sudo to administrative commands:
def ensure_sudo(command: str) -> str:
    if command.startswith("sudo "):
        return command
    prefixes = ["service ", "kill ", "pkill ", "rm ", "chmod ", "chown ",
                "apt ", "dpkg ", "nginx", "systemctl ", "fuser ", "docker "]
    for prefix in prefixes:
        if command.strip().startswith(prefix):
            return f"sudo {command.strip()}"
    return command
This ensures that even if the LLM forgets to include sudo, commands that require elevated privileges will execute correctly.

Environment Constraints

The system prompt includes detailed environment information:
"CONTEXTO:\n"
f"- Servicio afectado: {service}\n"
"- Te conectas via SSH como usuario 'sentinel' (NO root)\n"
"- TODOS los comandos de administracion necesitan 'sudo'\n"
"- Este es un servidor Ubuntu dentro de Docker\n"
"- Herramientas disponibles: ss, kill, pkill, ps, grep, cat, nginx, service\n"
"- NO tiene: systemctl, lsof, fuser\n\n"
The planner explicitly forbids tools that aren’t available on the target system (systemctl, lsof, fuser) to prevent generating non-executable plans.

Planning Rules

The LLM is constrained by strict rules enforced through the system prompt:

Command Count

Generate between 1 and 3 commands, one per line

No Chaining

Do NOT use &&, ||, or ; to chain commands

Sudo Requirement

ALL administrative commands must include sudo

No Repetition

NEVER repeat a command that already failed

Plain Output

Respond ONLY with commands, no explanation or backticks

Non-Interactive

Always use -y or —yes flags for package managers

Specialized Handling

The planner includes knowledge of common issues:
"7. Usa SIEMPRE la bandera '-y' o '--yes' en apt-get, yum, etc. para modo no interactivo.\n"
"8. SI hay error de 'lock' o 'dpkg interrupted', sugiere 'sudo dpkg --configure -a'.\n"
"9. PROHIBIDO usar: systemctl, lsof, fuser (no instalados).\n"
The non-interactive flag requirement prevents the system from hanging when package managers request confirmation.

Fallback Mechanism

If the LLM fails to generate valid commands, a safe default is used:
if not commands:
    commands = [f"sudo service {service} restart"]
    log("plan", "LLM no genero comandos validos. Usando fallback generico.")

State Updates

The planner returns the generated plan and sets approval status to pending:
return {
    "current_step": "plan",
    "candidate_plan": "\n".join(commands),
    "approval_status": "PENDING"
}

candidate_plan

Newline-separated list of commands

approval_status

Set to PENDING for security review

current_step

Marks workflow position

Command Format

Commands are stored as a newline-separated string:
plan_str = " -> ".join(commands)
log("plan", f"Comandos propuestos: {plan_str}")

return {
    "candidate_plan": "\n".join(commands)
}
Example output:
sudo service nginx stop
sudo pkill -9 nginx
sudo service nginx start

Memory Integration

The planner explicitly avoids repeating failed approaches:
if failed_commands:
    failed_str = "\nCOMANDOS QUE YA FALLARON (PROHIBIDO repetirlos):\n"
    for cmd in failed_commands:
        failed_str += f"- {cmd}\n"
This context is injected into the system prompt to guide the LLM away from known ineffective solutions.

Implementation Location

Source: src/agent/nodes/plan.py:24

Next Steps

After planning, the workflow transitions to the approval flow where commands are validated against security policies before execution.

Build docs developers (and LLMs) love