- Posted on
- • Artificial Intelligence
Artificial Intelligence Agent Planning Strategies
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Agent Planning Strategies for Bash Users: From “Think” to “Do” Safely
What if your nightly maintenance tasks, log triage, or “fix it and page me if it fails” routines could plan their own steps, verify each move, and back out safely? That’s the promise of agent planning: bringing structure and reliability to AI-driven or semi-automated operations. In this article, we’ll demystify agent planning strategies, explain why they matter, and give you practical Bash-friendly patterns and scripts you can use today—no exotic frameworks required.
Why Planning Matters (Even If You’re “Just” Scripting)
LLMs can draft steps, but naïve “one-shot” prompts are brittle. Planning provides scaffolding: goals, subgoals, tool calls, checks, and fallbacks.
In ops/SRE and DevSecOps, correctness beats charisma. Structured plans reduce blast radius by verifying intermediate states and encoding rollbacks.
Bash is already your orchestration layer. With a little structure (JSON plans +
jq+ checks), you can make your scripts behave like disciplined agents that think before they act.
Prerequisites (Install Once, Reuse Everywhere)
We’ll use standard CLI tools:
jq (for JSON plans)
graphviz (for plan visualization)
curl (for simple checks)
Install them with your package manager:
Debian/Ubuntu (apt):
sudo apt update sudo apt install -y jq graphviz curlFedora/RHEL/CentOS (dnf):
sudo dnf install -y jq graphviz curlopenSUSE (zypper):
sudo zypper install -y jq graphviz curl
Core Strategies You Can Use Today
1) Choose the Right Planning Pattern for the Job
Pick a strategy that matches problem complexity and risk:
Reactive (ReAct-style): Alternate “think” (decide next step) and “act” (run a tool), verifying after each step. Great for linear troubleshooting and CLI automation.
Task Decomposition: Break a goal into ordered subgoals and dependencies (a DAG). Ideal for maintenance playbooks and pipelines.
Tree Search (Tree-of-Thought/MCTS-inspired): Explore multiple candidate steps, score, and pick the best. Useful when ambiguity is high (e.g., root-cause analysis with multiple hypotheses). You can approximate this with branching plans and checks.
If you’re not sure, start with task decomposition. It’s easy to audit and integrates naturally with Bash.
2) Make Plans Data, Not Just Code
Represent each plan as JSON so it can be:
Generated by a human, a script, or an AI model
Visualized
Executed predictably and safely
Here’s a minimal plan for “Fix 502 from nginx” with dependencies, checks, and an optional rollback:
{
"goal": "Restore web service returning 502 from nginx",
"tasks": [
{
"id": "t1",
"desc": "Check nginx is running",
"cmd": "systemctl is-active --quiet nginx",
"check": "systemctl is-active --quiet nginx",
"deps": []
},
{
"id": "t2",
"desc": "Inspect nginx error log for upstream failures",
"cmd": "grep -m1 -E 'upstream.*(fail|timeout)' /var/log/nginx/error.log || true",
"check": ":",
"deps": []
},
{
"id": "t3",
"desc": "Probe upstream on port 8080",
"cmd": "curl -fsS http://127.0.0.1:8080/health -o /dev/null",
"check": "curl -fsS http://127.0.0.1:8080/health -o /dev/null",
"deps": ["t2"]
},
{
"id": "t4",
"desc": "Reload nginx if config is valid",
"cmd": "nginx -t && systemctl reload nginx",
"check": "sleep 1 && curl -fsS http://127.0.0.1/ -o /dev/null",
"rollback": "systemctl restart nginx || true",
"deps": ["t1", "t3"]
}
]
}
cmd: what to do
check: a verifiable condition that must pass
deps: task IDs that must complete first
rollback: an optional safe fallback if cmd/check fail
3) Execute with a Safe Planner-Executor Loop in Bash
The script below:
Resolves dependencies
Runs tasks with timeouts
Verifies checks
Optionally runs rollback
Logs all actions
Supports DRY_RUN and TIMEOUT
Save as plan-run.sh and chmod +x plan-run.sh:
#!/usr/bin/env bash
# plan-run.sh: Execute a JSON plan with deps, checks, and optional rollback.
# Usage: DRY_RUN=1 TIMEOUT=30 ROLLBACK_ON_FAIL=1 ./plan-run.sh plan.json
set -uo pipefail
PLAN_FILE="${1:-plan.json}"
LOG_FILE="run.log"
STATE_FILE="plan_state.json"
DRY_RUN="${DRY_RUN:-0}"
TIMEOUT_SECS="${TIMEOUT:-45}"
ROLLBACK_ON_FAIL="${ROLLBACK_ON_FAIL:-1}"
command -v jq >/dev/null || { echo "jq is required"; exit 1; }
command -v timeout >/dev/null || { echo "timeout is required (usually coreutils)"; exit 1; }
ts() { date -Is; }
log() { echo "[$(ts)] $*" | tee -a "$LOG_FILE" >&2; }
# Load task IDs
mapfile -t TASK_IDS < <(jq -r '.tasks[].id' "$PLAN_FILE")
# Initialize statuses (pending/done/failed)
declare -A STATUS
if [[ -f "$STATE_FILE" ]]; then
while IFS="=" read -r k v; do
[[ -n "$k" ]] && STATUS["$k"]="$v"
done < <(jq -r 'to_entries[] | "\(.key)=\(.value)"' "$STATE_FILE" 2>/dev/null || true)
fi
for id in "${TASK_IDS[@]}"; do
: "${STATUS[$id]:=pending}"
done
get_field() {
local id="$1" field="$2"
jq -r --arg id "$id" --arg f "$field" '.tasks[] | select(.id==$id) | .[$f] // ""' "$PLAN_FILE"
}
deps_done() {
local id="$1" dep
mapfile -t DEPS < <(jq -r --arg id "$id" '.tasks[] | select(.id==$id) | (.deps // [])[]' "$PLAN_FILE")
for dep in "${DEPS[@]}"; do
[[ "${STATUS[$dep]}" == "done" ]] || return 1
done
return 0
}
save_state() {
{
echo "{"
local first=1
for id in "${TASK_IDS[@]}"; do
[[ $first -eq 1 ]] || echo ","
first=0
printf ' "%s": "%s"' "$id" "${STATUS[$id]}"
done
echo
echo "}"
} > "$STATE_FILE"
}
progress_made=1
while [[ $progress_made -eq 1 ]]; do
progress_made=0
for id in "${TASK_IDS[@]}"; do
[[ "${STATUS[$id]}" == "pending" ]] || continue
if deps_done "$id"; then
desc="$(get_field "$id" desc)"
cmd="$(get_field "$id" cmd)"
check="$(get_field "$id" check)"
rollback="$(get_field "$id" rollback)"
log "READY: [$id] $desc"
if [[ "$DRY_RUN" == "1" ]]; then
log "DRY_RUN: would run: $cmd"
[[ -n "$check" ]] && log "DRY_RUN: would verify: $check"
STATUS[$id]="done"
save_state
progress_made=1
continue
fi
ok=0
if [[ -n "$cmd" ]]; then
log "RUN: $cmd"
if timeout "$TIMEOUT_SECS" bash -lc "$cmd" >>"$LOG_FILE" 2>&1; then
ok=1
else
ok=0
fi
else
ok=1
fi
if [[ $ok -eq 1 && -n "$check" ]]; then
log "CHECK: $check"
if timeout "$TIMEOUT_SECS" bash -lc "$check" >>"$LOG_FILE" 2>&1; then
ok=1
else
ok=0
fi
fi
if [[ $ok -eq 1 ]]; then
log "SUCCESS: [$id]"
STATUS[$id]="done"
else
log "FAIL: [$id]"
if [[ "$ROLLBACK_ON_FAIL" == "1" && -n "$rollback" ]]; then
log "ROLLBACK: $rollback"
timeout "$TIMEOUT_SECS" bash -lc "$rollback" >>"$LOG_FILE" 2>&1 || true
fi
STATUS[$id]="failed"
fi
save_state
progress_made=1
fi
done
done
# Summary
failed_any=0
for id in "${TASK_IDS[@]}"; do
st="${STATUS[$id]}"
log "TASK [$id]: $st"
[[ "$st" == "failed" ]] && failed_any=1
if [[ "$st" == "pending" ]]; then
log "BLOCKED: [$id] has unmet deps; check for cycles or failures"
failed_any=1
fi
done
exit $failed_any
Run it:
./plan-run.sh plan.json
# or safely test first
DRY_RUN=1 ./plan-run.sh plan.json
This is already an “agent”: it respects dependencies, verifies intermediate outcomes, and contains failure policy (rollback).
4) Visualize Your Plan (DAG) to Review Risk
Before you run, visualize the dependency graph to catch surprises. Save as plan-viz.sh:
#!/usr/bin/env bash
# Usage: ./plan-viz.sh plan.json | dot -Tpng -o plan.png
set -euo pipefail
PLAN="${1:-plan.json}"
echo "digraph Plan {"
jq -r '.tasks[] | @base64' "$PLAN" | while read -r row; do
obj() { echo "$row" | base64 --decode | jq -r "$1"; }
id="$(obj '.id')"
desc="$(obj '.desc' | @sh)"
echo " \"$id\" [label=$desc, shape=box, style=rounded];"
jq -r "$row | @base64d | fromjson | .deps // [] | .[]" | while read -r dep; do
echo " \"$dep\" -> \"$id\";"
done
done
echo "}"
Then:
./plan-viz.sh plan.json | dot -Tpng -o plan.png
Open plan.png to visually validate ordering and fan-out/fan-in. This doubles as documentation for code review.
5) Build Safety In: Limits, Dry-Runs, and Telemetry
Dry-run first:
DRY_RUN=1 ./plan-run.sh plan.jsonLimit blast radius:
- Use
timeout(already included). - Run destructive steps behind explicit flags in your plan (e.g., require
ALLOW_RESTART=1). - Consider
sudo -nso plans never hang on a password prompt.
- Use
Log everything: the runner appends to
run.logand writesplan_state.jsonfor postmortem.Idempotence wins: Make
checkidempotent and side-effect-free so reruns are safe.
Real-World Examples You Can Adapt
Log Triage: Plan lists log locations, greps key error signatures, counts frequencies, and emits a suggested next action. Checks verify logs exist and aren’t empty.
Disk Pressure Remediation: Decompose into “identify big dirs,” “archive or purge old artifacts,” “verify free space,” and “notify.” Rollback reverts a mount or restores a tar.
Blue/Green Deploy NGINX: Validate config, warm up upstream, flip symlink or config block, reload, and health-check. Rollback flips back on failure.
Each of these is just a different JSON plan feeding the same runner.
Bonus: Prompt Template to Auto-Draft Plans
If you do use an LLM, hand it this template and ask for valid JSON matching our schema:
You are a planning agent. Produce a JSON object with fields:
- goal: string
- tasks: array of {id, desc, cmd, check, rollback?, deps[]}
Constraints:
- Each id is unique and simple (e.g., t1, t2).
- cmds are Bash-safe and non-interactive.
- checks succeed if and only if the step achieved its goal.
- Use only tools available on a typical Linux server.
- Keep it minimal, safe, and idempotent.
User goal: "Restore web service returning 502 from nginx"
Paste the result into plan.json, review it, visualize, dry-run, then execute for real.
Conclusion and Call to Action
Agent planning isn’t just for research labs—it maps cleanly onto the way Bash users already work: goals, ordered steps, checks, and fallbacks. You can start small:
1) Install jq, graphviz, and curl (see commands above).
2) Save the runner and visualizer scripts.
3) Wrap one repetitive task as a JSON plan.
4) Visualize it, dry-run it, then execute.
5) Iterate: add rollbacks, better checks, and branchier plans as needed.
Next step: pick one maintenance routine you dread and convert it to a plan. Your future self (and your pager) will thank you.