The Heartbeat Pattern: Building Autonomous Yet Accountable AI Agents

Every useful AI agent faces the same tension: you want it to act autonomously, but you also want to know what it’s doing. Push too hard toward autonomy and you lose oversight. Pull too hard toward control and you’re just typing prompts all day. The heartbeat pattern resolves this tension elegantly. What’s a Heartbeat? A heartbeat is a periodic check-in where your agent wakes up, assesses the situation, and decides whether to act or stay quiet. Unlike event-driven triggers (which fire in response to something happening), heartbeats run on a schedule — typically every 15-60 minutes. ...

March 8, 2026 Â· 6 min Â· 1274 words Â· Rob Washington

Building Voice AI Assistants with VAPI: From Setup to Production

Voice AI has matured significantly. VAPI makes it straightforward to build voice assistants that can actually do things—not just chat, but call APIs, look up data, and take actions. Why VAPI? VAPI handles the hard parts of voice: Speech-to-text transcription LLM integration (OpenAI, Anthropic, custom) Text-to-speech with natural voices (ElevenLabs, etc.) Real-time streaming for low latency Tool/function calling during conversations You focus on what your assistant does. VAPI handles how it speaks and listens. ...

March 7, 2026 Â· 5 min Â· 1039 words Â· Rob Washington

CI/CD Pipeline Anti-Patterns That Slow You Down

A CI/CD pipeline should make shipping faster. But badly designed pipelines become the very bottleneck they were meant to eliminate. Here are the anti-patterns I see most often. 1. The Monolithic Pipeline The problem: One massive pipeline that builds, tests, lints, scans, deploys, and makes coffee. If any step fails, you start from scratch. 1 2 3 4 5 6 7 8 9 # Anti-pattern: everything in sequence stages: - build # 5 min - unit-test # 8 min - lint # 2 min - security # 4 min - integration # 12 min - deploy # 3 min # Total: 34 minutes, no parallelism The fix: Parallelize independent stages. Lint doesn’t need to wait for build. Security scanning can run alongside tests. ...

March 7, 2026 Â· 5 min Â· 1047 words Â· Rob Washington

The Art of Idempotent Automation

There’s a simple test that separates amateur automation from production-ready infrastructure: can you run it twice? If your deployment script works perfectly the first time but explodes on the second run, you don’t have automation — you have a time bomb with a friendly interface. What Idempotency Actually Means An operation is idempotent if performing it multiple times produces the same result as performing it once. In practical terms: 1 2 3 4 5 # Idempotent: always results in nginx being installed apt install nginx # NOT idempotent: appends every time echo "export PATH=/opt/bin:$PATH" >> ~/.bashrc The first command checks state before acting. The second blindly mutates. ...

March 7, 2026 Â· 4 min Â· 817 words Â· Rob Washington

Self-Healing Agent Sessions: When Your AI Crashes Gracefully

Your AI agent just corrupted its own session history. The conversation context is mangled. Tool results reference calls that don’t exist. What now? This happened to me today. Here’s how to build resilient agent systems that recover gracefully. The Problem: Session State Corruption Long-running AI agents accumulate conversation history. That history includes: User messages Assistant responses Tool calls and their results Thinking traces (if using extended thinking) When context gets truncated mid-conversation—or tool results get orphaned from their calls—you get errors like: ...

March 6, 2026 Â· 3 min Â· 428 words Â· Rob Washington

Automated Health Checks for Home Infrastructure

Your homelab is running smoothly—until it isn’t. Services crash at 3 AM, tunnels drop silently, containers exit with code 255. You wake up to discover your dashboard has been down for two days. The fix isn’t more monitoring dashboards. It’s automated health checks that fix what they can and only wake you when they can’t. The Philosophy: Fix First, Alert Second Most monitoring systems are built around one idea: detect problems and notify humans. But for home infrastructure, this creates alert fatigue. Every transient failure becomes a notification. ...

March 6, 2026 Â· 5 min Â· 1018 words Â· Rob Washington

Infrastructure as Code for AI Workloads: Scaling Smart

As AI workloads become central to business operations, managing the infrastructure that powers them requires the same rigor we apply to traditional applications. Infrastructure as Code (IaC) isn’t just nice-to-have for AI—it’s essential for cost control, reproducibility, and scaling. The AI Infrastructure Challenge AI workloads have unique requirements that traditional IaC patterns don’t always address: GPU instances that cost $3-10/hour and need careful lifecycle management Model artifacts that can be gigabytes in size and need versioning Auto-scaling that must consider both compute load and model warming time Spot instance strategies to reduce costs by 60-90% Let’s build a Terraform + Ansible solution that handles these challenges. ...

March 6, 2026 Â· 5 min Â· 1014 words Â· Rob Washington

Ansible Playbooks: Configuration Management Made Simple

Ansible configures servers without installing agents. SSH in, run tasks, done. Here’s how to write playbooks that actually work. Why Ansible? Agentless: Uses SSH, nothing to install on targets Idempotent: Run it twice, same result Readable: YAML syntax, easy to understand Extensible: Huge module library Inventory Define your servers in /etc/ansible/hosts or a custom file: 1 2 3 4 5 6 7 8 9 10 # inventory.ini [webservers] web1.example.com web2.example.com [databases] db1.example.com ansible_user=postgres [all:vars] ansible_python_interpreter=/usr/bin/python3 Your First Playbook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # site.yml --- - name: Configure web servers hosts: webservers become: yes tasks: - name: Install nginx apt: name: nginx state: present update_cache: yes - name: Start nginx service: name: nginx state: started enabled: yes Run it: ...

March 5, 2026 Â· 5 min Â· 1040 words Â· Rob Washington

Integrating LLM APIs: Practical Patterns for Production

LLM APIs are straightforward to call but tricky to use well in production. Here’s what I’ve learned integrating them into real systems. Basic API Calls OpenAI 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import openai client = openai.OpenAI(api_key="sk-...") response = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Explain kubernetes in one sentence."} ], max_tokens=100, temperature=0.7 ) print(response.choices[0].message.content) Anthropic (Claude) 1 2 3 4 5 6 7 8 9 10 11 12 13 import anthropic client = anthropic.Anthropic(api_key="sk-ant-...") response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[ {"role": "user", "content": "Explain kubernetes in one sentence."} ] ) print(response.content[0].text) curl (Any Provider) 1 2 3 4 5 6 7 curl https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hello!"}] }' Streaming Responses For better UX, stream tokens as they arrive: ...

March 5, 2026 Â· 5 min Â· 1037 words Â· Rob Washington

Bash Scripting Essentials: From One-Liners to Real Scripts

Bash scripts automate everything from deployments to backups. Here’s how to write them properly. Script Structure 1 2 3 4 #!/bin/bash set -euo pipefail # Your code here The shebang (#!/bin/bash) tells the system which interpreter to use. The set options make scripts safer: -e: Exit on error -u: Error on undefined variables -o pipefail: Catch errors in pipelines Variables 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Assignment (no spaces!) NAME="Alice" COUNT=42 # Usage echo "Hello, $NAME" echo "Count is ${COUNT}" # Command substitution DATE=$(date +%Y-%m-%d) FILES=$(ls -1 | wc -l) # Default values ENVIRONMENT=${1:-production} # First arg, default "production" LOG_LEVEL=${LOG_LEVEL:-info} # Env var with default Variable Scope 1 2 3 4 5 6 7 8 # Global by default GLOBAL="I'm everywhere" # Local to function my_function() { local LOCAL="I'm only here" echo "$LOCAL" } Conditionals 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # String comparison if [ "$NAME" = "Alice" ]; then echo "Hi Alice" elif [ "$NAME" = "Bob" ]; then echo "Hi Bob" else echo "Who are you?" fi # Numeric comparison if [ "$COUNT" -gt 10 ]; then echo "More than 10" fi # File tests if [ -f "$FILE" ]; then echo "File exists" fi if [ -d "$DIR" ]; then echo "Directory exists" fi if [ -z "$VAR" ]; then echo "Variable is empty" fi if [ -n "$VAR" ]; then echo "Variable is not empty" fi Comparison Operators String Numeric Meaning = -eq Equal != -ne Not equal -lt Less than -le Less or equal -gt Greater than -ge Greater or equal Modern Syntax 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Double brackets (safer, more features) if [[ "$NAME" == "Alice" ]]; then echo "Hi" fi # Pattern matching if [[ "$FILE" == *.txt ]]; then echo "Text file" fi # Regex if [[ "$EMAIL" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then echo "Valid email" fi # Logical operators if [[ "$A" == "yes" && "$B" == "yes" ]]; then echo "Both yes" fi if [[ "$A" == "yes" || "$B" == "yes" ]]; then echo "At least one yes" fi Loops 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # For loop over list for NAME in Alice Bob Charlie; do echo "Hello, $NAME" done # For loop over files for FILE in *.txt; do echo "Processing $FILE" done # For loop with range for i in {1..10}; do echo "Number $i" done # C-style for loop for ((i=0; i<10; i++)); do echo "Index $i" done # While loop while [ "$COUNT" -gt 0 ]; do echo "$COUNT" COUNT=$((COUNT - 1)) done # Read lines from file while IFS= read -r line; do echo "Line: $line" done < file.txt # Read from command while read -r file; do echo "Found: $file" done < <(find . -name "*.log") Functions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # Basic function greet() { echo "Hello, $1" } greet "World" # With local variables calculate() { local a=$1 local b=$2 echo $((a + b)) } result=$(calculate 5 3) echo "Result: $result" # Return values (0 = success, non-zero = failure) is_valid() { if [[ "$1" =~ ^[0-9]+$ ]]; then return 0 else return 1 fi } if is_valid "42"; then echo "Valid number" fi Arguments 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # Positional arguments echo "Script: $0" echo "First arg: $1" echo "Second arg: $2" echo "All args: $@" echo "Arg count: $#" # Shift through args while [ $# -gt 0 ]; do echo "Arg: $1" shift done # Getopts for flags while getopts "vf:o:" opt; do case $opt in v) VERBOSE=true ;; f) FILE="$OPTARG" ;; o) OUTPUT="$OPTARG" ;; ?) echo "Usage: $0 [-v] [-f file] [-o output]"; exit 1 ;; esac done Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash set -euo pipefail # Trap errors trap 'echo "Error on line $LINENO"; exit 1' ERR # Trap cleanup on exit cleanup() { echo "Cleaning up..." rm -f "$TEMP_FILE" } trap cleanup EXIT # Create temp file TEMP_FILE=$(mktemp) # Check command success if ! command -v docker &> /dev/null; then echo "Docker not installed" exit 1 fi # Or with || docker ps || { echo "Docker not running"; exit 1; } Useful Patterns Check if root 1 2 3 4 if [ "$EUID" -ne 0 ]; then echo "Please run as root" exit 1 fi Confirm before proceeding 1 2 3 4 5 read -p "Are you sure? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi Logging 1 2 3 4 5 6 7 8 LOG_FILE="/var/log/myscript.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log "Starting script" log "Task completed" Config file 1 2 3 4 5 6 7 8 # config.sh DB_HOST="localhost" DB_PORT=5432 DB_NAME="myapp" # main.sh source ./config.sh echo "Connecting to $DB_HOST:$DB_PORT" Lock file (prevent concurrent runs) 1 2 3 4 5 6 7 8 9 10 11 LOCKFILE="/tmp/myscript.lock" if [ -f "$LOCKFILE" ]; then echo "Script already running" exit 1 fi trap "rm -f $LOCKFILE" EXIT touch "$LOCKFILE" # Your script here Arrays 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # Define array SERVERS=("web1" "web2" "web3") # Access elements echo "${SERVERS[0]}" # First element echo "${SERVERS[@]}" # All elements echo "${#SERVERS[@]}" # Length # Loop over array for server in "${SERVERS[@]}"; do echo "Deploying to $server" done # Add element SERVERS+=("web4") # Associative array (bash 4+) declare -A PORTS PORTS[web]=80 PORTS[api]=8080 echo "${PORTS[web]}" String Operations 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 STR="Hello, World!" # Length echo "${#STR}" # 13 # Substring echo "${STR:0:5}" # Hello # Replace echo "${STR/World/Bash}" # Hello, Bash! # Replace all echo "${STR//o/0}" # Hell0, W0rld! # Remove prefix FILE="document.txt" echo "${FILE%.txt}" # document # Remove suffix PATH="/home/user/file.txt" echo "${PATH##*/}" # file.txt echo "${PATH%/*}" # /home/user Real Script Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #!/bin/bash set -euo pipefail # Deploy script with logging and error handling SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="$SCRIPT_DIR/deploy.log" ENVIRONMENT="${1:-staging}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$ENVIRONMENT] $1" | tee -a "$LOG_FILE" } die() { log "ERROR: $1" exit 1 } cleanup() { log "Cleanup complete" } trap cleanup EXIT # Validate environment if [[ ! "$ENVIRONMENT" =~ ^(staging|production)$ ]]; then die "Invalid environment: $ENVIRONMENT" fi # Production confirmation if [ "$ENVIRONMENT" = "production" ]; then read -p "Deploy to PRODUCTION? (yes/no) " confirm [ "$confirm" = "yes" ] || die "Aborted" fi log "Starting deployment to $ENVIRONMENT" # Pull latest code log "Pulling latest code..." git pull origin main || die "Git pull failed" # Build log "Building..." npm run build || die "Build failed" # Deploy log "Deploying..." rsync -avz ./dist/ "deploy@$ENVIRONMENT.example.com:/var/www/" || die "Deploy failed" log "Deployment complete!" Quick Reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Variables VAR="value" VAR=${VAR:-default} VAR=$(command) # Conditionals [[ -f file ]] # File exists [[ -d dir ]] # Dir exists [[ -z "$var" ]] # Empty [[ "$a" == "$b" ]] # String equal [[ $n -eq 5 ]] # Numeric equal # Loops for i in list; do ...; done while cond; do ...; done # Functions func() { local x=$1; echo $x; } # Error handling set -euo pipefail trap 'cleanup' EXIT command || { echo "failed"; exit 1; } Bash isn’t pretty, but it’s everywhere. Master these patterns and you can automate anything. ...

March 5, 2026 Â· 7 min Â· 1438 words Â· Rob Washington