Skip to content

Capstone: Building the Agentic Triad

This capstone integrates three systems from across the curriculum into a single running pipeline: a Gemini CLI workspace auditor, a Hermes self-improving agent, and an OpenClaw gateway that exposes both through Telegram. When complete, you can trigger multi-agent workflows from a phone message and receive structured results.

All three components run as Docker services on your Linux machine. Telegram is the unified control surface. The Gemini CLI is the development-time code auditor.


Prerequisites

Complete before starting this capstone:

  • M09: Docker + gVisor configured (docker run --runtime=runsc works)
  • M11: LangGraph state schema concepts
  • M12: Hermes agent installation and docker-compose up working
  • M13: OpenClaw gateway installed, Telegram bot token in hand
  • M15: Langfuse tracing running locally
bash
# Verify all services
docker ps | grep -E "(hermes|openclaw|langfuse|postgres)"
ollama list | grep llama-guard3

Application 1: Gemini CLI Workspace Auditor

A terminal utility that monitors Python scripts for architectural violations. Uses Gemini CLI context injection to audit files on demand.

Configure workspace context

bash
cd ~/AI_BOOTCAMP
mkdir -p .gemini/commands/audit

Create .gemini/GEMINI.md — the workspace rules Gemini CLI loads automatically:

markdown
# AI_BOOTCAMP Workspace Audit Rules

## Enforcement Standards
- All async functions must use `await`, not `time.sleep()` or `requests.*`
- All function arguments and return values require type hints
- No hardcoded credentials, API keys, or passwords — use `os.environ`
- Pydantic v2 models must use `model_config = ConfigDict(...)` not class `Config`
- Docker Compose files must not expose databases on 0.0.0.0

## Reporting Format
Output a markdown table with columns: Line | Violation | Severity | Fix
After the table, output a fully corrected version of the file.

Create the audit slash command at .gemini/commands/audit/code.toml:

toml
description = "Audits a Python file for async violations, type safety, and credential leaks."
prompt = """
You are a senior Python architect performing a security and quality audit.
Apply the rules from GEMINI.md to the injected file context.

Report format:
1. Violations table (Line | Violation | Severity | Fix)
2. Risk summary (Critical/High/Medium/Low counts)
3. Corrected file

Be specific about line numbers. If the file is clean, say so explicitly.
"""

Run an audit

bash
# Launch Gemini CLI
npx @google/gemini-cli

# Inject a file for audit — @ injects the file content into context
gemini> /audit:code @./labs/event-automation/webhook_handler.py

The auditor reads your workspace rules from GEMINI.md and applies them to whatever file you inject. Run it before every commit on agent-facing code.

Automate audits with a shell wrapper

bash
# audit.sh — audit any Python file from the terminal
#!/bin/bash
TARGET="${1:-}"
if [ -z "$TARGET" ]; then
    echo "Usage: ./audit.sh path/to/file.py"
    exit 1
fi

if [ ! -f "$TARGET" ]; then
    echo "File not found: $TARGET"
    exit 1
fi

echo "Auditing: $TARGET"
npx @google/gemini-cli --prompt "/audit:code @$TARGET" --no-interactive
bash
chmod +x audit.sh
./audit.sh labs/hermes-agent/skill_executor.py

Application 2: Hermes Self-Improving Agent

A persistent research assistant that writes code, sandboxes it, catches failures, self-heals, and registers successful implementations as reusable skills.

Boot Hermes

bash
cd ~/AI_BOOTCAMP/labs/hermes-agent
docker-compose up -d

# Confirm containers are up
docker-compose ps
# hermes-core    running
# postgres       running

Connect and assign a task

bash
docker exec -it hermes-agent-core hermes-cli

Assign a task that requires Hermes to discover its own dependencies — the self-healing loop only fires when the first execution fails:

hermes> Create a new skill called 'scrape_ai_news'.
Specifications:
- Target: https://news.ycombinator.com
- Parse the top 10 posts. Filter for titles containing 'AI', 'LLM', or 'agent'.
- Format each as: "## [Title](url)\nScore: N | Comments: N"
- Save markdown to /app/data/ai_newsletter.md
- Return a summary of how many posts matched the filter.

Observe the self-healing loop

Watch the console. The sequence should be:

[hermes] Drafting skill: scrape_ai_news
[hermes] Executing in sandbox...
[sandbox] ModuleNotFoundError: No module named 'beautifulsoup4'
[hermes] Installing missing dependency: beautifulsoup4
[sandbox] ModuleNotFoundError: No module named 'requests'
[hermes] Installing missing dependency: requests
[hermes] Retry 3: SUCCESS
[hermes] Compiling skill to /app/skills/scrape_ai_news.py
[hermes] Registering in memory store...
[hermes] Skill registered. 7 posts matched filter.

Each failure is caught, the missing import is installed inside the sandbox, and execution retries. The successful implementation is persisted to the skills library and the pgvector memory store.

Verify the skill is registered

python
# check_skills.py — run from outside the container
import psycopg2
import os

conn = psycopg2.connect(
    host="localhost",
    port=5432,
    database=os.environ["POSTGRES_DB"],
    user=os.environ["POSTGRES_USER"],
    password=os.environ["POSTGRES_PASSWORD"],
)
cursor = conn.cursor()

cursor.execute("""
    SELECT name, created_at, execution_count
    FROM agent_skills
    ORDER BY created_at DESC
    LIMIT 5;
""")

for row in cursor.fetchall():
    print(f"Skill: {row[0]} | Created: {row[1]} | Runs: {row[2]}")

conn.close()
bash
# Load credentials from .env, never hardcode
set -a && source .env && set +a
python3 check_skills.py

Application 3: OpenClaw Remote Gateway

OpenClaw receives Telegram messages, parses them as commands, and dispatches to registered skills — including triggering Hermes tasks and querying the pgvector memory store.

Environment setup

bash
cd ~/AI_BOOTCAMP/labs/openclaw
cp .env.example .env

Edit .env — fill real values, never commit this file:

bash
TELEGRAM_BOT_TOKEN=123456789:REPLACE_WITH_YOUR_BOT_TOKEN
TELEGRAM_ALLOWED_USER_IDS=REPLACE_WITH_YOUR_TELEGRAM_USER_ID
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=hermes
POSTGRES_USER=agent_user
POSTGRES_PASSWORD=REPLACE_WITH_YOUR_POSTGRES_PASSWORD
PORT=9000
DATA_DIR=~/AI_BOOTCAMP/labs/openclaw/data

Write skills that connect to Hermes

python
# skills/hermes_bridge.py
from openclaw.skills import skill
import subprocess
import psycopg2
import os
import logging

logger = logging.getLogger(__name__)


def _get_db_conn():
    return psycopg2.connect(
        host=os.environ["POSTGRES_HOST"],
        port=os.environ["POSTGRES_PORT"],
        database=os.environ["POSTGRES_DB"],
        user=os.environ["POSTGRES_USER"],
        password=os.environ["POSTGRES_PASSWORD"],
    )


@skill(description="Runs the Hermes AI news scraping skill and returns the summary.")
def run_news_scrape() -> str:
    try:
        result = subprocess.run(
            ["docker", "exec", "hermes-agent-core", "hermes", "run", "scrape_ai_news"],
            capture_output=True,
            text=True,
            timeout=120,
        )
        if result.returncode != 0:
            logger.error(f"Hermes skill failed: {result.stderr[:200]}")
            return f"Scrape failed: {result.stderr[:100]}"
        return result.stdout.strip() or "Scrape complete. Check /app/data/ai_newsletter.md"
    except subprocess.TimeoutExpired:
        return "Hermes skill timed out after 120s."
    except Exception as e:
        logger.exception("Unexpected error running Hermes skill")
        return f"Error: {e}"


@skill(description="Returns the last N items from Hermes long-term memory.")
def recall_memories(n: int = 5) -> str:
    try:
        conn = _get_db_conn()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT content, created_at FROM agent_longterm_memories "
            "ORDER BY created_at DESC LIMIT %s;",
            (n,)
        )
        rows = cursor.fetchall()
        conn.close()

        if not rows:
            return "No memories stored yet."
        return "\n".join(f"- {row[0]} ({row[1].strftime('%Y-%m-%d')})" for row in rows)
    except Exception as e:
        logger.exception("Failed to query memory store")
        return f"Memory query failed: {e}"


@skill(description="Lists all compiled Hermes skills.")
def list_skills() -> str:
    try:
        conn = _get_db_conn()
        cursor = conn.cursor()
        cursor.execute(
            "SELECT name, execution_count FROM agent_skills ORDER BY execution_count DESC;"
        )
        rows = cursor.fetchall()
        conn.close()
        return "\n".join(f"• {r[0]} ({r[1]} runs)" for r in rows) or "No skills registered."
    except Exception as e:
        return f"Failed: {e}"

Launch OpenClaw

bash
cd ~/AI_BOOTCAMP/labs/openclaw
set -a && source .env && set +a
nvm use default
npm install
npm run start

Test from Telegram

Send these messages to your bot:

/run_news_scrape
/list_skills
/recall_memories 3

You should receive formatted responses pulled from your Hermes agent's memory store and skill execution results.


Application 4: The Unified Triad Pipeline

All three components running together. The workflow:

  1. You open Telegram and message: /run_news_scrape
  2. OpenClaw receives the webhook, authenticates your Telegram user ID, routes to run_news_scrape()
  3. The skill triggers docker exec hermes-agent-core hermes run scrape_ai_news
  4. Hermes executes the compiled skill inside the gVisor sandbox
  5. Results write to /app/data/ai_newsletter.md and pgvector memory
  6. OpenClaw sends the summary back to your Telegram chat

Meanwhile, from your development terminal:

  • Run ./audit.sh labs/hermes-agent/skill_executor.py to check the code quality
  • The Gemini CLI auditor reads GEMINI.md rules and flags any violations before you deploy changes

Bring everything up

bash
# Terminal 1: Hermes + Postgres
cd ~/AI_BOOTCAMP/labs/hermes-agent && docker-compose up

# Terminal 2: OpenClaw
cd ~/AI_BOOTCAMP/labs/openclaw && set -a && source .env && set +a && npm run start

# Terminal 3: Monitor
watch -n 2 'docker stats --no-stream'

Add Langfuse tracing to the gateway

Every OpenClaw dispatch goes through Langfuse so you can trace latency, token counts, and self-healing cycles from the dashboard:

python
# skills/traced_bridge.py
from langfuse import Langfuse
from langfuse.decorators import observe
import os

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host=os.environ.get("LANGFUSE_HOST", "https://cloud.langfuse.com"),
)


@observe(name="openclaw.news_scrape")
def run_news_scrape_traced() -> str:
    from hermes_bridge import run_news_scrape
    return run_news_scrape()

Open the Langfuse dashboard (http://localhost:3000 if self-hosted) and watch traces appear as you send Telegram commands.


Runtime Verification

bash
# All containers healthy
docker-compose -f ~/AI_BOOTCAMP/labs/hermes-agent/docker-compose.yml ps
docker-compose -f ~/AI_BOOTCAMP/labs/openclaw/docker-compose.yml ps

# OpenClaw is receiving webhooks (tail the log)
docker logs openclaw-gateway -f --tail=20

# Hermes memory is being written
psql -h localhost -U $POSTGRES_USER -d $POSTGRES_DB \
  -c "SELECT count(*) FROM agent_longterm_memories;"

# Langfuse is recording traces
curl http://localhost:3000/api/health  # {"status":"ok"}

Failure mode reference:

SymptomCauseFix
Telegram bot not respondingTELEGRAM_ALLOWED_USER_IDS wrongGet your ID via @userinfobot on Telegram
Hermes exec failsContainer not named hermes-agent-coreRun docker ps and check the Names column, update the exec command
DB connection refusedPostgres port not exposed in docker-composeAdd ports: - "5432:5432" to the postgres service
Memory queries return emptyHermes skill ran but memory write failedCheck POSTGRES_* env vars are loaded in Hermes container
Langfuse traces not appearingWrong host or keysecho $LANGFUSE_PUBLIC_KEY — confirm env is loaded