Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hermes

A self-improving personal AI agent: a terminal agent loop with persistent, searchable memory and a skill library it writes to itself. Modeled after the architecture of NousResearch/hermes-agent ("the agent that grows with you"), scoped down to the core loop: no multi-backend deployment, no messaging gateway -- just the learning loop, built clean and working end to end.

What it does

  • Agent loop. Streams a response from an LLM, executes any tool calls it makes, feeds the results back, and repeats until the model is done talking. Provider-agnostic: Anthropic, OpenAI, or OpenRouter, switchable via config with no code changes.
  • Persistent memory. Every message from every session is written to a local SQLite database with an FTS5 full-text index, so the agent can search_memory across months of past conversations instead of forgetting everything when the process exits.
  • Skills. When a turn takes enough tool calls to look like a real procedure, the agent is handed the transcript and asked to distill it into a skill: a versioned markdown file with a name, description, trigger keywords, and a step-by-step body. Skills live in ~/.hermes/skills/*.md (plain text, human-readable, compatible in spirit with the agentskills.io convention), and reusing one bumps its use count; refining one bumps its version.
  • CLI. A REPL with slash commands for the above (/skills, /memory, /learn, /new, /model), multiline input and history via prompt_toolkit, colored streaming output via rich.
  • Web dashboard. A password-gated Flask app (hermes/web/) exposing the same AgentLoop through a browser: a chat panel with streaming responses and inline tool-call/tool-result rendering, a skills browser, a memory search panel, and session history -- deployable to Railway.

Architecture

hermes/
  config.py              # env -> Config (provider, model, paths)
  cli.py                 # REPL: prompt_toolkit input, rich output, slash commands
  __main__.py             # `python -m hermes` / `hermes` entry point

  providers/              # provider-agnostic LLM client layer
    base.py                # Message / ToolSpec / ToolCall / StreamChunk / LLMProvider
    anthropic_provider.py   # Claude, native tool_use streaming
    openai_compatible.py    # OpenAI + OpenRouter (same wire format, different base_url)
    registry.py             # get_provider(config) factory

  agent/
    prompts.py              # the actual prompts -- see below
    tools.py                # ToolRegistry: run_bash, read/write/list_dir,
                             # search_memory, list_skills, use_skill, create_skill
    loop.py                 # AgentLoop.run_turn(): stream -> execute tools -> repeat
                             # -> learning trigger

  memory/
    store.py                # MemoryStore: SQLite + FTS5, sessions + messages + search

  skills/
    schema.py                # Skill dataclass <-> markdown+YAML-frontmatter file
    manager.py                # SkillManager: create / get / list / search / refine / record_use

  learning/
    curator.py                # after a complex turn: summarize_session(),
                               # propose_skill() (asks the model to author a skill,
                               # parses the response, saves or refines it)

  web/
    state.py                  # AppState: one provider/memory/skills + a pool of
                               # live AgentLoops keyed by browser session id
    app.py                    # create_app(): password gate, chat/skills/memory/
                               # session JSON+NDJSON APIs
    templates/, static/       # login page + single-page dashboard (vanilla JS)

wsgi.py                        # gunicorn entry point: `gunicorn wsgi:app`

The agent loop, concretely

  1. User types something. It's appended to conversation history and written to the memory DB.
  2. AgentLoop.run_turn streams the model's response through the active provider. Text deltas are yielded for the CLI to print live; tool calls are buffered.
  3. If the model made tool calls, each is executed by ToolRegistry and the result is fed back into history as a tool-result message; the loop goes back to step 2. This repeats until the model responds with no further tool calls, or a hard iteration cap is hit (25, to stop runaway loops).
  4. If the turn used at least 3 tool calls (a proxy for "this was a real, multi-step task"), the full transcript is handed to learning/curator.propose_skill, which asks the model to either distill it into a new/refined skill or say NONE.
  5. On session close, the transcript is summarized (curator.summarize_session) and the summary is stored against the session row, so a future search_memory hit can be understood at a glance without re-reading the whole conversation.

The prompts

This is the actual behavioral spec, in full, in hermes/agent/prompts.py:

  • MAIN_SYSTEM_PROMPT -- the agent's identity, its tool list, and its operating rules (check for an existing skill before solving from scratch, use search_memory before claiming ignorance, only write a skill for something genuinely reusable, don't fabricate tool output, be direct).
  • SKILL_AUTHORING_PROMPT -- given a transcript, forces a strict NAME / DESCRIPTION / TRIGGERS / BODY format (or NONE) so the response can be parsed deterministically and turned into a skill file.
  • SESSION_SUMMARY_PROMPT -- asks for a short first-person note to future-self about what happened in the session, for memory recall.

Install

Requires Python 3.10+.

git clone <this-repo> hermes
cd hermes
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env   # fill in HERMES_PROVIDER + the matching API key
hermes

Configuration (.env)

Variable Meaning
HERMES_PROVIDER anthropic | openai | openrouter
HERMES_MODEL model id for that provider (defaults per-provider if unset)
ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY only the one matching HERMES_PROVIDER is required
HERMES_HOME where memory.sqlite3 and skills/ live (default ~/.hermes)
HERMES_WORKSPACE root directory the file/bash tools are confined to (default: cwd)
FLASK_SECRET_KEY web dashboard only; required, no default
HERMES_WEB_PASSWORD web dashboard only; required unless HERMES_WEB_ALLOW_NO_AUTH=true
HERMES_WEB_ENABLE_SHELL / HERMES_WEB_ENABLE_WRITE web dashboard only; default false each

Usage

$ hermes
hermes -- anthropic:claude-sonnet-5 -- workspace /Users/you/project
Type /help for commands, /exit to quit.

hermes> deploy this flask app to railway
-> run_bash({'command': 'railway login'})
...
learned a new skill: deploy-flask-to-railway

hermes> /skills
deploy-flask-to-railway (used 1x) -- Deploy a Flask app to Railway with gunicorn

hermes> /memory railway
session 1 [assistant] [...deploy the app with `railway up`...]

Web dashboard

Run it locally:

pip install -e .
export FLASK_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
export HERMES_WEB_PASSWORD=pick-something-strong
flask --app wsgi:app run

Open http://localhost:5000, log in with HERMES_WEB_PASSWORD. The chat panel streams the same AgentLoop the CLI uses; the right panel has live /api/skills and /api/memory/search views, the left sidebar lists past sessions (click one to view its transcript read-only) and starts new ones.

By default the web surface runs with run_bash and write_file disabled regardless of the CLI's tool set -- see Safety notes below for why, and how to opt back in with HERMES_WEB_ENABLE_SHELL / HERMES_WEB_ENABLE_WRITE.

Deploying to Railway

railway init          # or `railway link` to an existing project
railway volume create -m /data       # persistent disk for memory.sqlite3 + skills
railway variables set \
  HERMES_PROVIDER=anthropic \
  HERMES_MODEL=claude-sonnet-5 \
  ANTHROPIC_API_KEY=sk-ant-... \
  FLASK_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))") \
  HERMES_WEB_PASSWORD=pick-something-strong \
  HERMES_HOME=/data \
  HERMES_WORKSPACE=/data/workspace
railway up

Procfile / railway.json run gunicorn wsgi:app with one worker, multiple threads (--workers 1 --threads 4) -- deliberately, since the loop pool (hermes/web/state.py) and the in-process login-attempt throttle are plain Python dicts, not shared across worker processes. Don't raise --workers without moving that state into something external (Redis, a database row, etc.).

Mount the volume at HERMES_HOME (/data above) or memory and skills reset on every redeploy. Point HERMES_WORKSPACE at an empty directory on that same volume, not the app's own source checkout -- see Safety notes.

Safety notes

run_bash executes arbitrary shell commands with the permissions of whatever user runs hermes, confined only to a working directory (cwd=) -- it is not sandboxed, containerized, or permission-gated the way the upstream Hermes agent's Docker/SSH/Modal/Daytona backends are. read_file / write_file / list_dir do refuse to resolve outside HERMES_WORKSPACE, but run_bash can still touch anything that directory's owner can. Don't point HERMES_WORKSPACE at anything you wouldn't hand a junior engineer unsupervised shell access to, and don't wire this up to an untrusted input source without adding a real sandbox first.

The web dashboard raises the stakes on this, since it's a chat UI reachable from the internet rather than a terminal only you can type into. Mitigations already in place:

  • run_bash and write_file are excluded from the tool list entirely on the web surface by default (HERMES_WEB_ENABLE_SHELL / HERMES_WEB_ENABLE_WRITE, both default false) -- the model never even sees them as available tools.
  • The whole app is behind a single shared password (HERMES_WEB_PASSWORD), checked with a constant-time comparison, with a simple in-memory lockout after 5 failed attempts from one IP in 5 minutes. FLASK_SECRET_KEY is mandatory; the app refuses to start without it.
  • create_app() refuses to start at all if HERMES_WEB_PASSWORD is unset, unless you explicitly set HERMES_WEB_ALLOW_NO_AUTH=true (local/trusted networks only -- never on a public deployment).

Even with the gate up: point HERMES_WORKSPACE at an empty, disposable directory when running the web app, not the repo checkout itself, so read_file/list_dir have nothing sensitive to expose if the password ever leaks. This is a personal single-user tool, not a hardened multi-tenant service -- don't share the password with anyone you wouldn't hand your API key to directly.

Testing

pip install -e ".[dev]"
pytest

Covers the memory store (session/message roundtrip, FTS5 search including special-character queries, session summaries), the skill manager (create, duplicate rejection, use-count/version bumps, search), the tool registry (file I/O, workspace path-traversal rejection, bash execution, skill create/list/use), and the web app (auth gate on / and /api/*, login success/failure, fail-closed startup when FLASK_SECRET_KEY or HERMES_WEB_PASSWORD is missing) -- all without needing a live API key. The provider and agent-loop streaming/tool-calling paths are exercised by manual smoke testing (CLI and web dashboard) but have no API-key-free unit coverage yet, since they require a live model.

What's deliberately out of scope (vs. upstream Hermes)

No Docker/SSH/Singularity/Modal/Daytona backends, no Telegram/Discord/ Slack/WhatsApp/Signal gateway, no cron scheduler, no Honcho user modeling, no MCP server support, no voice transcription. This is the core learning loop -- agent + memory + skills + CLI -- built to actually run, not a stub of the full product surface.

License

MIT

About

Self-improving AI agent: streaming tool-calling loop, persistent SQLite+FTS5 memory, and a skill library it writes and refines itself. Core-MVP reimplementation of NousResearch/hermes-agent.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages