# Async Hermes Agent β€” Full Documentation This file contains every Markdown and MDX page currently published in the Async Hermes Agent documentation. Canonical site: https://ykoh42.github.io/async-hermes-agent/ Short index: https://ykoh42.github.io/async-hermes-agent/llms.txt --- # Async Hermes Agent Documentation import Link from "@docusaurus/Link"; import useBaseUrl from "@docusaurus/useBaseUrl"; # Async Hermes Agent The Hermes Agent harness, converted to an async-only Python library. It keeps the upstream agent loop, tools, providers, MCP, skills, memory, sessions, trajectories, runner, and batch runner behind coroutine entry points.
{"Get Started β†’"} {"Python Library"} {"View on GitHub"}
## Install Python 3.11 through 3.13 is supported. Install the currently verified release from PyPI: ```bash uv pip install "async-hermes-agent==0.20.1.2" ``` PyPI publication uses OIDC Trusted Publishing. The same verified wheel, source distribution, and checksums are attached to the corresponding GitHub Release. Then follow the **[Installation Guide](/getting-started/installation)** to verify the package and choose only the provider dependencies your application needs. :::tip Fastest path to a working agent Choose any supported provider and a current tool-capable model, then follow the [Quickstart](/getting-started/quickstart). No provider is privileged by the library or by this documentation. ::: ## What is Async Hermes Agent? Async Hermes Agent is an async-only distribution of the upstream [NousResearch/Hermes Agent](https://github.com/NousResearch/hermes-agent), based on release `v2026.8.13`. Existing library integrations keep familiar module paths and public names; I/O-bearing calls are awaited. The host application owns its HTTP service or UI and the agent remains the reusable harness beneath it. ## Quick Links | | | | ------------------------------------------------------------- | -------------------------------------------------------------------------- | | πŸš€ **[Installation](/getting-started/installation)** | Install a tagged release or create a development checkout | | πŸ“– **[Quickstart Tutorial](/getting-started/quickstart)** | Run your first awaited conversation | | 🐍 **[Python Library](/guides/python-library)** | Lifecycle, return values, concurrency, cancellation, and service ownership | | βš™οΈ **[Configuration](/user-guide/configuration)** | Providers, models, toolsets, skills, MCP, memory, and sessions | | πŸ”§ **[Tools & Toolsets](/user-guide/features/tools)** | Retained tools and model-facing toolset control | | πŸ“š **[Skills System](/user-guide/features/skills)** | Discover, read, and manage reusable skill documents | | πŸ”Œ **[MCP Integration](/user-guide/features/mcp)** | Connect stdio, Streamable HTTP, and SSE servers | | 🧠 **[Memory System](/user-guide/features/memory)** | Persistent memory and user-profile context | | πŸ’Ύ **[Sessions](/user-guide/sessions)** | Async SQLite persistence and resume | | πŸ§ͺ **[Batch Processing](/user-guide/features/batch-processing)** | Generate resumable interleaved-thinking trajectories | | πŸ—οΈ **[Architecture](/developer-guide/architecture)** | Understand the native-async narrow waist | | ❓ **[FAQ & Troubleshooting](/reference/faq)** | Common integration and migration questions | :::caution Native-file implementation boundary Provider, network, MCP, and subprocess paths use coroutine transports. The regular-file layer uses `aiofiles`, which delegates disk operations to an executor, and `aiosqlite` executes SQLite calls on a connection worker thread. The supported Python 3.11–3.13 releasesβ€”and [Python 3.14](https://docs.python.org/3.14/library/asyncio-eventloop.html#working-with-pipes)β€”have no portable asyncio regular-file or embedded-SQLite API. The library therefore promises directly awaitable, event-loop-nonblocking entry points rather than zero-thread, OS-native regular-file or SQLite I/O. ::: ## Key Features - **Async library surface** β€” Model, tool, MCP, session, trajectory, runner, and batch-runner entry points are awaited under their upstream names - **Familiar upstream surface** β€” Core file locations, import paths, public names, arguments, and return shapes stay recognizable; I/O calls add `await` - **Extensible tool use** β€” Retained terminal, file, web, browser, vision, skills, memory, session-search, clarification, delegation, and plugin tools - **MCP support** β€” Dynamic stdio, Streamable HTTP, and SSE server discovery with ordered tool observations and explicit async lifecycle cleanup - **Persistent context** β€” SQLite sessions, memory, user-profile context, FTS history search, checkpoints, and resume - **Training-data harness** β€” Preserve reasoning, tool calls, observations, and final answers; generate JSONL trajectories with bounded batch concurrency - **Service-ready library** β€” Embed the agent in FastAPI or another async host without bundling a second service framework or HTTP contract - **Upstream-traceable** β€” Derived from Hermes Agent `v2026.8.13` with original source paths retained to make later migrations reviewable ## For LLMs and coding agents - llms.txt provides a compact documentation index. - llms-full.txt concatenates the maintained documentation. Both files are generated from this site during the documentation build. --- # Installation # Installation Start here. Async Hermes Agent is distributed as a Python library; your application supplies its own service or UI boundary. ## Requirements - Python 3.11, 3.12, or 3.13 - Git - A model provider credential, unless you use a local endpoint ## 1. Install the current release Install the exact reviewed package version from PyPI: ```bash uv pip install "async-hermes-agent==0.20.1.2" ``` The release workflow publishes through OIDC Trusted Publishing. The same verified wheel, source distribution, and `SHA256SUMS` file are attached to the [`v0.20.1.2` GitHub Release](https://github.com/ykoh42/async-hermes-agent/releases/tag/v0.20.1.2). To install the reviewed source snapshot instead, pin the immutable tag: ```bash uv pip install "git+https://github.com/ykoh42/async-hermes-agent.git@v0.20.1.2" ``` ## Version policy The four numeric segments keep the fork tied to its upstream baseline. `0.20.1.2` means upstream Python package version `0.20.1` (tag `v2026.8.13`) plus async-distribution revision `2`. Fork-only releases increment the final segment; a completed upstream port changes the first three segments and resets the final segment to `1`. The earlier `0.20.4` GitHub release used an independent scheme. Python version tag should be explicitly reinstalled at the new upstream-aligned version: ```bash uv pip install --reinstall "async-hermes-agent==0.20.1.2" ``` ## 2. Or install from a checkout ```bash git clone https://github.com/ykoh42/async-hermes-agent.git cd async-hermes-agent uv sync ``` For development and tests: ```bash uv sync --extra dev uv run pytest -q ``` ## 3. Add provider-specific extras The base installation includes the OpenAI-compatible async transport, MCP, SQLite session storage, and the core tool runtime. Provider selection is a separate decision: install only the extras required by the provider family you choose. | Extra | Use | | --- | --- | | `anthropic` | Native Anthropic API | | `vertex` | Google Vertex credentials and async transport support | | `azure-identity` | Restricted Microsoft Entra ID support; see provider limitations | | `bedrock` | AWS Bedrock transport; the pinned SDK still has blocking bootstrap boundaries | | `parallel-web` | Parallel web search provider | | `fal` | FAL image/video generation | | `edge-tts` | Edge text-to-speech backend | | `tts-premium` | ElevenLabs text-to-speech backend | | `mistral` | Mistral text-to-speech backend | | `piper-tts` | Local Piper backend, isolated in a profile-scoped subprocess broker | | `modal` | Modal execution backend | | `daytona` | Daytona execution backend | | `vercel` | Vercel Sandbox execution backend | | `mem0` | Mem0 memory provider, including optional OSS dependencies | | `supermemory` | Supermemory memory provider | | `hindsight` | Hindsight memory provider client; embedded-local mode also needs `hindsight-all` | | `honcho` | Honcho memory provider | | `postgres` | Opt-in PostgreSQL SessionDB backend (SQLAlchemy Core + asyncpg; run the real integration matrix before production use) | | `dev` | Test, lint, leak-check, and type-check dependencies | KittenTTS 0.8.1 is published by KittenML as a GitHub release wheel rather than an official PyPI distribution. PyPI rejects direct URL dependencies in uploaded package metadata, so this project cannot offer it as a normal extra. The public-index package named `kittentts` is not the compatible KittenML 0.8.1 artifact. Install the hash-verified official wheel when selecting `tts.provider: kittentts`; the wheel declares its own `soundfile` dependency: ```bash python -m pip install \ 'https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl#sha256=482a436c4f1f3192153710376e459ff3689517ebcda7c2b051e2fd4187b41851' ``` Piper is available through the package extra: ```bash python -m pip install 'async-hermes-agent[piper-tts]' ``` The compatibility extras `exa`, `firecrawl`, `homeassistant`, `computer-use`, `vision`, and `mcp` currently add no Python distributions: their retained Python dependencies are already in the base install. They keep the upstream install names valid. External runtimes are still separate; for example, `computer-use` requires a working `cua-driver` installation. From a checkout, for example: ```bash uv sync --extra anthropic uv sync --extra bedrock ``` Missing provider dependencies fail with an installation hint; the library does not silently move synchronous provider code into a worker thread. ## 4. Verify the async API ```bash python - <<'PY' import inspect from run_agent import AIAgent assert inspect.iscoroutinefunction(AIAgent.run_conversation) assert inspect.iscoroutinefunction(AIAgent.chat) assert inspect.iscoroutinefunction(AIAgent.close) print("native-async API available") PY ``` This verifies the installed interface without making a paid model request. Continue with the [Quickstart](./quickstart.md). It begins with a neutral provider-selection step, then runs the same awaited agent API for every route. --- # Learning Path # Learning Path Start with [Installation](./installation.md), then complete the [Quickstart](./quickstart.md). Everything below assumes one provider and one tool-capable model already complete a normal awaited turn. ## By goal | Goal | Recommended reading | | --- | --- | | Embed the agent in Python | [Python Library](../guides/python-library.md) β†’ [Configuration](../user-guide/configuration.md) β†’ [Sessions](../user-guide/sessions.md) | | Expose it from an async service | [Programmatic Integration](../developer-guide/programmatic-integration.md) β†’ [Security](../user-guide/security.md) β†’ [Session Storage](../developer-guide/session-storage.md) | | Generate fine-tuning trajectories | [Batch Processing](../user-guide/features/batch-processing.md) β†’ [Trajectory Format](../developer-guide/trajectory-format.md) β†’ [Agent Loop](../developer-guide/agent-loop.md) | | Add model-facing capabilities | [Tools](../user-guide/features/tools.md) β†’ [Skills](../user-guide/features/skills.md) β†’ [MCP](../user-guide/features/mcp.md) | | Understand or extend the harness | [Architecture](../developer-guide/architecture.md) β†’ [Agent Loop](../developer-guide/agent-loop.md) β†’ [Toolsets Reference](../reference/toolsets-reference.md) | ## Recommended order for training work 1. Verify one provider can return reasoning and valid tool calls. 2. Inspect the exact tool schemas and enabled toolsets. 3. Run a single trajectory with `save_trajectories=True`. 4. Confirm reasoning β†’ tool call β†’ observation β†’ final answer ordering. 5. Run a small resumable `BatchRunner` job. 6. Add dataset quality gates and model training outside this package. The harness generates trajectories; it does not decide whether a sample is correct, safe, diverse, or suitable for a particular fine-tuning objective. ## Recommended order for service work 1. Use one `AIAgent` per ordered conversation. 2. Map application identity to a deliberate session-isolation policy. 3. Register callbacks for interactive tools or disable those tools. 4. Bound concurrent agents and external tool work. 5. Propagate cancellation and always `await agent.close()`. 6. Add authentication, quotas, and HTTP schemas in the host application. You do not need to read every page. Follow the path matching the boundary your application owns, then return to the feature and reference sections as needed. --- # Platform Support # Platform Support Async Hermes Agent is a Python library rather than the complete Hermes desktop, CLI, or messaging distribution. Platform support therefore follows the Python runtime and the external tools enabled by your application. ## Supported Python versions | Python | Status | | --- | --- | | 3.11 | Supported and used by CI | | 3.12 | Supported | | 3.13 | Supported | | 3.10 and older | Unsupported | | 3.14 and newer | Not yet supported by the package constraint | ## Operating systems The core agent, provider, session, trajectory, skill, and MCP code is intended for Linux, macOS, and Windows. A capability can impose additional requirements: - terminal and process behavior follows the host shell and process model; - browser tools require their external browser runtime; - stdio MCP servers require their configured command to exist on `PATH`; - optional cloud providers require their provider extra and credentials; - filesystem paths and subprocess environments remain platform-specific. Linux is the authoritative CI environment. Before production deployment on a different platform, run the repository test suite and an end-to-end model β†’ tool β†’ observation β†’ final-answer turn on that target. ## Supported installation methods Install the verified PyPI distribution and pin the package version in production: ```bash uv pip install "async-hermes-agent==0.20.1.2" ``` The same verified distributions and checksums are attached to the GitHub Release. Installing the immutable Git tag or a reviewed source checkout is also supported. This site does not claim a Homebrew, desktop-installer, container-image, or system-service distribution. A host application may package the library in those forms, but owns that additional support contract. Continue with [Installation](./installation.md) and the [Quickstart](./quickstart.md). --- # Quickstart # Quickstart This guide starts after installation, lets you choose any supported provider, and runs one complete conversation. The public API keeps the upstream module and method names: add `await`; there is no separate `arun_*` API. ## 1. Install and verify Follow the [Installation guide](./installation.md) and run its async API check before configuring a provider. ## 2. Choose a provider and model Pick the route that matches your deployment. No bundled provider profile is the default or the assumed path. | Route | Use it when | What the host supplies | | --- | --- | --- | | Bundled provider profile | You use one of the provider profiles shipped with the library | Provider name, model ID, and that provider's credential or identity | | Custom OpenAI-compatible endpoint | You operate vLLM, SGLang, Ollama, LM Studio, or another compatible server | `custom`, base URL, model ID, and any required key | | File-backed defaults | Several agent instances share one route | Non-secret model settings in `$HERMES_HOME/config.yaml`; credentials in `.env` | See the complete [Provider catalog](../integrations/providers.md) before picking a credential or optional dependency. For a provider-neutral runnable example, let the host application expose four ordinary application variables: ```bash export MODEL_PROVIDER="" export MODEL_ID="" export MODEL_API_KEY="" # Only custom or overridden routes need this: export MODEL_BASE_URL="" ``` These are example variables owned by your host application, not additional Hermes configuration keys. Provider catalogs, aliases, availability, and prices change over time, so this guide does not pin a vendor or model name. ## 3. Run one conversation ```python import asyncio import os from run_agent import AIAgent async def main() -> None: async with AIAgent( provider=os.environ["MODEL_PROVIDER"], base_url=os.getenv("MODEL_BASE_URL") or None, api_key=os.getenv("MODEL_API_KEY") or None, model=os.environ["MODEL_ID"], enabled_toolsets=["file", "terminal"], ) as agent: result = await agent.run_conversation( "Inspect the current project and identify its main modules." ) print(result["final_response"]) asyncio.run(main()) ``` `AIAgent.__init__()` performs state-only construction. The async context manager initializes provider, plugin, and MCP resources and always awaits `close()`. ## 4. Choose the return shape `run_conversation()` returns the full turn result. Its stable core fields are the final response, message history, and completion state. Normal completed turns also carry session and routing metadata, while early terminal/error results may omit those optional fields: ```python result = await agent.run_conversation("Use the available tools if needed.") print(result["final_response"]) print(result["messages"]) print(result.get("session_id")) ``` For a string-only result, use `chat()`: ```python answer = await agent.chat("Summarize your findings in three bullets.") ``` When not using `async with`, close explicitly: ```python agent = AIAgent(...) try: answer = await agent.chat("Hello") finally: await agent.close() ``` ## Concurrency behavior Turns submitted concurrently to one `AIAgent` instance are serialized so they cannot corrupt a shared conversation. Separate instances can overlap I/O-bound work. Use one instance for an ordered conversation and separate instances for independent requests. Next, see [Configuration](../user-guide/configuration.md), the [Python library guide](../guides/python-library.md), [Tools](../user-guide/features/tools.md), and [Sessions](../user-guide/sessions.md). --- # Updating & Uninstalling # Updating & Uninstalling Async Hermes Agent does not include the upstream interactive updater. The host application controls dependency upgrades, rollout, and rollback. ## Update a pinned installation Replace `` with the release you have reviewed: ```bash uv pip install --upgrade "async-hermes-agent==" ``` Pinning the release keeps the agent harness reproducible alongside your model, prompt, and dataset versions. If you install from source, pin an immutable tag or commit rather than tracking `main` implicitly. ### One-time migration from the legacy version scheme GitHub releases through `0.20.4` used a fork-only version sequence. Starting with `0.20.1.2`, the first three numeric segments match the upstream Python package and the fourth is this distribution's revision. This release aligns the fork with upstream `v2026.8.13`; replace an old exact pin and reinstall it explicitly: ```bash uv pip install --reinstall "async-hermes-agent==0.20.1.2" ``` Also update lockfiles, requirements manifests, and direct Git URLs from `v0.20.4` to `v0.20.1.2`. Fork-only follow-ups increment the final segment, while a later upstream port changes the first three segments and resets the revision to `1`. ## Update a source checkout ```bash git fetch origin --tags git status --short git diff --stat HEAD..origin/main git pull --ff-only origin main uv sync --locked ``` Inspect local changes before updating. This fork keeps upstream file locations to make migrations reviewable, but native-async changes can still conflict with a newer Hermes Agent release. ## Validate after updating Run all of the following before rollout: 1. `uv lock --check` 2. `uv run ruff check .` 3. the repository's native-async audit from `.github/workflows/ci.yml` 4. `scripts/run_tests.sh -j 4 -- -q` 5. one real provider turn that exercises a tool and persists its trajectory 6. your application's cancellation, session-resume, and shutdown tests ## Roll back Reinstall the previous tag or deploy the previous lockfile. Session, memory, skill, and trajectory data live under `HERMES_HOME`; back that directory up according to your application's retention policy before a schema-sensitive upgrade. ## Uninstall ```bash uv pip uninstall async-hermes-agent ``` Uninstalling the package does not remove `HERMES_HOME`. Delete application state only when you have explicitly decided that sessions, memories, skills, credentials, and trajectories are no longer needed. --- # Configuration # Configuration Async Hermes Agent has no setup wizard. Configure an agent with explicit constructor arguments and use files for shared capability settings. ## Configuration locations `HERMES_HOME` defaults to `~/.hermes`: ```text ~/.hermes/ β”œβ”€β”€ config.yaml # Non-secret behavior and capability settings β”œβ”€β”€ .env # API keys and tokens β”œβ”€β”€ skills/ # Application-installed skills β”œβ”€β”€ memories/ # File-backed memory and user profile └── plugins/ # Application-owned plugins ``` Select another isolated home before constructing an agent: ```bash export HERMES_HOME=/srv/my-agent-state ``` The files are read lazily at an awaited lifecycle boundary. For tests, point `HERMES_HOME` at a temporary directory before importing or constructing runtime objects. ## Prefer explicit model arguments Service code is easiest to reason about when model routing is explicit: ```python agent = AIAgent( provider=os.environ["MODEL_PROVIDER"], base_url=os.getenv("MODEL_BASE_URL") or None, api_key=os.getenv("MODEL_API_KEY") or None, model=os.environ["MODEL_ID"], enabled_toolsets=["file", "terminal", "skills"], ) ``` `enabled_toolsets` and `disabled_toolsets` are per-agent controls. They are the recommended replacement for upstream interface-specific toolset configuration. ## Keep secrets separate Use the process environment or `$HERMES_HOME/.env` for credentials: ```dotenv ANTHROPIC_API_KEY=... GOOGLE_API_KEY=... DEEPSEEK_API_KEY=... TEAM_MCP_TOKEN=... ``` Use `config.yaml` for non-secret settings. Environment references are supported where a nested configuration needs a secret: ```yaml mcp_servers: team: url: "https://mcp.example.com/mcp" headers: Authorization: "Bearer ${env:TEAM_MCP_TOKEN}" ``` Do not commit `.env` or place raw credentials in examples, logs, trajectories, or model prompts. ## Minimal capability configuration Only include sections your application uses: ```yaml model: provider: "" default: "" # base_url: "" memory: memory_enabled: true user_profile_enabled: true memory_char_limit: 2200 user_char_limit: 1375 skills: external_dirs: - /srv/team-skills browser: cloud_provider: local security: allow_private_urls: false ``` MCP server definitions are documented in [Use MCP with Hermes](../guides/use-mcp-with-hermes.md). Model settings are covered in [Configuring models](./configuring-models.md). ## Configuration lifetime The system-prompt cached prefix and base tool selection are intentionally stable for a conversation to preserve prompt caching. MCP- and plugin-derived tool snapshots may refresh at a turn boundary, but remain fixed during that turn. Do not mutate other configuration and expect an active conversation to rebuild its past context. Apply those changes by creating a new agent lifecycle unless a documented capability explicitly supports refresh. The upstream `cli-config.yaml.example` contains settings for product surfaces that are not shipped by this library. It is not a canonical library configuration reference. --- # Configuring Models # Configuring Models Every agent needs a provider route and a model identifier. Model names are controlled by external providers and are not fixed in these docs. ## Explicit per-agent configuration ```python import os from run_agent import AIAgent agent = AIAgent( provider=os.environ["MODEL_PROVIDER"], base_url=os.getenv("MODEL_BASE_URL") or None, api_key=os.getenv("MODEL_API_KEY") or None, model=os.environ["MODEL_ID"], max_iterations=30, ) ``` This form is recommended for services, evaluations, and batch jobs because the route is visible in code and can be injected by the host application. ## File-based defaults For shared defaults, write `$HERMES_HOME/config.yaml`: ```yaml model: provider: "" default: "" # base_url: "" ``` Put the credential in `$HERMES_HOME/.env`: ```dotenv =... ``` Explicit constructor values override defaults for that instance. Keep API keys out of `config.yaml` even when a provider accepts them there. The exact credential variable is listed in the [Provider catalog](../integrations/providers.md). ## Native providers and extras OpenAI-compatible endpoints use the base installation. Native Anthropic, Vertex, Entra ID, and Bedrock routes need their corresponding optional extras. See [Model providers](../integrations/providers.md) for the transport matrix and [Installation](../getting-started/installation.md) for install commands. ## Reasoning and output limits Pass provider-supported reasoning controls explicitly: ```python agent = AIAgent( ..., reasoning_config={"enabled": True, "effort": "low"}, max_tokens=4096, ) ``` Provider support varies. `max_tokens` limits one generated response; it is not the model's total context window. Do not assume that every provider accepts the same reasoning effort names or returns reasoning content. For trajectory generation, choose a route that actually returns reasoning. `BatchRunner` deliberately excludes samples with no recorded assistant reasoning from its merged training trajectories. ## Custom or local endpoint ```python agent = AIAgent( provider="custom", base_url="http://127.0.0.1:8000/v1", api_key="local", model="served-model-name", ) ``` The server must implement a compatible message and tool-call schema. The library does not install or supervise the model server. ## Validate behavior, not only connectivity A successful text reply does not prove a model can operate this harness. Before production use, exercise: - a model β†’ tool call β†’ tool observation β†’ final response turn; - reasoning capture if trajectories require it; - the intended context length and compression path; - cancellation and timeout behavior; - provider usage fields if billing reports depend on them. Unsupported synchronous transports fail at initialization rather than running inside a hidden thread. --- # Batch Processing # Batch Processing `BatchRunner` runs a JSONL prompt dataset through the same agent loop used by interactive library calls. It is a training-data and evaluation harness, not a fine-tuning trainer. ## Input Each non-empty JSONL row must contain `prompt`: ```jsonl {"prompt":"Inspect this Python project and explain its async boundaries."} {"prompt":"Use the file tools to repair the supplied test fixture."} ``` Malformed rows and rows without `prompt` are skipped. Keep each dataset item self-contained: batch runs set `skip_context_files=True`, `skip_memory=True`, and do not attach durable sessions. ## Run programmatically There is no supported command-line entrypoint in this library distribution. Construct `BatchRunner` and await its existing `run()` method: ```python import asyncio import os from batch_runner import BatchRunner async def main() -> None: runner = BatchRunner( dataset_file="prompts.jsonl", batch_size=8, run_name="tool-training", distribution="terminal_only", base_url=os.getenv("MODEL_BASE_URL") or None, api_key=os.getenv("MODEL_API_KEY") or None, model=os.environ["MODEL_ID"], num_workers=4, max_iterations=20, reasoning_config={"enabled": True, "effort": "low"}, ) await runner.run(resume=True) asyncio.run(main()) ``` `run()` returns `None`; its contract is the files written below. ## Concurrency model Each batch processes its prompts sequentially, preserving shard order. Up to `num_workers` batches overlap model and tool I/O through asyncio tasks and a semaphore. `num_workers` itself does not create a batch worker pool. Checkpoint and shard file operations use the package's current `aiofiles` layer and can therefore use its executor-backed regular-file implementation. Cancellation cancels and awaits sibling batch tasks. If cancellation arrives after a shard-row append has started, that single append completes before `CancelledError` propagates. Checkpoint publication uses atomic replacement to avoid half-written resume state. ## Output Files are written under `data//`: ```text data/tool-training/ β”œβ”€β”€ batch_0.jsonl β”œβ”€β”€ batch_1.jsonl β”œβ”€β”€ trajectories.jsonl β”œβ”€β”€ checkpoint.json └── statistics.json ``` - `batch_*.jsonl` are append-only per-batch shards. - `trajectories.jsonl` merges valid shard rows. - `checkpoint.json` records completed prompts. - `statistics.json` summarizes tool and reasoning coverage. Resume scans existing batch files and matches successful samples by prompt content before scheduling remaining work. Failed prompts can be retried. ## Trajectory contract The `conversations` sequence uses ShareGPT-style `from`/`value` entries with roles such as `system`, `human`, `gpt`, and `tool`. Reasoning, model-emitted tool calls, ordered observations, and the final answer are preserved. Tool statistics and error counts are normalized for dataset processing. Samples with zero recorded assistant reasoning are discarded from trajectory files and marked complete, so resume does not repeatedly regenerate them. Invalid JSON and entries containing unknown/hallucinated tool names are excluded when shards are merged. For a single conversation, `AIAgent(save_trajectories=True)` appends successful samples to `trajectory_samples.jsonl` in the current working directory and failed samples to `failed_trajectories.jsonl`. --- # Browser Automation # Browser Automation The browser toolset exposes accessibility-tree navigation and native CDP-backed operations to the model. Browser work is awaited and its session is closed with the agent lifecycle. ## Tool surface The retained tools include navigation, accessibility snapshots, click, type, scroll, back, key press, image collection, screenshot vision, console access, CDP operations, and dialog handling. Enable them with: ```python agent = AIAgent(..., enabled_toolsets=["browser"]) ``` Accessibility snapshots assign stable element references for later click and type calls. Screenshot analysis requires a configured vision-capable model. ## Local mode Local mode uses the external `agent-browser` command and a Chromium-family browser. Those Node/browser components are not installed by this Python package: ```bash npm install -g agent-browser agent-browser install ``` Select local mode explicitly: ```yaml browser: cloud_provider: local headed: false engine: auto ``` `headed: true` opens a visible browser. Supported engine values are `auto`, `chrome`, and `lightpanda`, subject to the installed `agent-browser` version. ## Existing CDP browser Attach to a Chrome/Chromium-compatible debugging endpoint without a UI command: ```yaml browser: cloud_provider: local cdp_url: "http://127.0.0.1:9222" ``` The runtime resolves the browser WebSocket endpoint and uses the retained CDP supervisor. Protect remote CDP endpoints as credentials: control URLs can grant complete access to browser tabs, cookies, and authenticated sessions. ## Cloud providers The retained browser plugins are: | Provider key | Credentials | | --- | --- | | `browser-use` | `BROWSER_USE_API_KEY` | | `browserbase` | `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID` | | `firecrawl` | `FIRECRAWL_API_KEY` | For example: ```yaml browser: cloud_provider: browserbase ``` Set credentials in the environment or `$HERMES_HOME/.env`. External account availability, billing, anti-bot behavior, proxies, and session limits are owned by the selected service. ## Private-network protection Navigation guards block private, loopback, link-local, and cloud-metadata targets by default. Keep the preferred global setting disabled: ```yaml security: allow_private_urls: false ``` With a cloud provider selected, `browser.auto_local_for_private_urls` defaults to `true`: an explicitly requested private URL can be routed to a local browser sidecar instead of sending it to the cloud provider, while redirect-based private-network access remains guarded. Disable hybrid routing with: ```yaml browser: cloud_provider: browserbase auto_local_for_private_urls: false ``` Enabling private URLs widens SSRF reach and should be confined to an isolated agent and network. See [Security](../security.md). --- # LSP β€” Semantic Diagnostics # Language Server Protocol (LSP) Async Hermes runs full language servers β€” pyright, gopls, rust-analyzer, typescript-language-server, clangd, and ~20 more β€” as background subprocesses and feeds their semantic diagnostics into the post-write lint check used by `write_file` and `patch`. When the agent edits a file, it sees exactly the errors that edit introduced β€” not just syntax errors, but **type errors, undefined names, missing imports, and project-wide semantic issues** the language server detects. This is the same architecture top-tier coding agents use. Hermes ships it self-contained: no editor host required, no plugin to install, no separate daemon to manage. ## When LSP runs LSP is gated on **git workspace detection**. When the agent's working directory (or the file being edited) is inside a git repository, LSP runs against that workspace. When neither is in a git repo, LSP stays dormant β€” useful for messaging gateways where the cwd is the application's neutral working directory and there's no project to diagnose. The check is layered: in-process syntax check first (microseconds), then LSP diagnostics second when syntax is clean. A flaky or missing language server can never break a write β€” every LSP failure path falls back silently to the syntax-only result. Concretely, on every successful `write_file` or `patch`: 1. Hermes captures a baseline of current diagnostics for the file. 2. Performs the write. 3. Re-queries the language server, filters out diagnostics that were already in the baseline, and surfaces only the new ones. The agent sees output like: ``` { "bytes_written": 42, "dirs_created": false, "lint": {"status": "ok", "output": ""}, "lsp_diagnostics": "LSP diagnostics introduced by this edit:\n\nERROR [42:5] Cannot find name 'foo' [reportUndefinedVariable] (Pyright)\nERROR [50:1] Argument of type \"str\" is not assignable to \"int\" [reportArgumentType] (Pyright)\n" } ``` The `lint` field carries the syntax-check result (microsecond in-process parse via `ast.parse`, `json.loads`, etc.); the `lsp_diagnostics` field carries the semantic diagnostics from the real language server. Two channels, independent signals β€” the agent sees a syntax-clean file with semantic problems as ``lint: ok`` plus a populated ``lsp_diagnostics``. All LSP file reads, installation commands, JSON-RPC traffic, diagnostic waits, and server shutdowns stay on the caller's event loop. There is no thread wrapper or background event loop. `AIAgent` instances share servers on that loop; the last agent to close releases them: ```python from run_agent import AIAgent async with AIAgent(...) as agent: await agent.chat("Fix the type errors in src/") ``` Code that invokes the file-tool layer without an `AIAgent` owns the shared service boundary explicitly: ```python from agent.lsp import shutdown_service from tools.file_tools import write_file_tool try: result = await write_file_tool("src/app.py", content) finally: await shutdown_service() ``` ## Supported languages | Language | Server | Auto-install | |----------|--------|--------------| | Python | `pyright-langserver` | npm | | TypeScript / JavaScript / JSX / TSX | `typescript-language-server` | npm | | Vue | `@vue/language-server` | npm | | Svelte | `svelte-language-server` | npm | | Astro | `@astrojs/language-server` | npm | | Go | `gopls` | `go install` | | Rust | `rust-analyzer` | manual (rustup) | | C / C++ | `clangd` | manual (LLVM) | | Bash / Zsh | `bash-language-server` | npm | | YAML | `yaml-language-server` | npm | | Lua | `lua-language-server` | manual (GitHub releases) | | PHP | `intelephense` | npm | | OCaml | `ocaml-lsp` | manual (opam) | | Dockerfile | `dockerfile-language-server-nodejs` | npm | | Terraform | `terraform-ls` | manual | | Dart | `dart language-server` | manual (dart sdk) | | Haskell | `haskell-language-server` | manual (ghcup) | | Julia | `julia` + LanguageServer.jl | manual | | Clojure | `clojure-lsp` | manual | | Nix | `nixd` | manual | | Zig | `zls` | manual | | Gleam | `gleam lsp` | manual (gleam install) | | Elixir | `elixir-ls` | manual | | Prisma | `prisma language-server` | manual | | Kotlin | `kotlin-language-server` | manual | | Java | `jdtls` | manual | | PowerShell | `PowerShellEditorServices` (`pwsh` host) | manual (release zip) | For "manual" entries, install the server through whatever toolchain manager makes sense for that language (rustup, ghcup, opam, brew, …). Hermes auto-detects the binary on PATH or in `/lsp/bin/`. ### PowerShell PowerShellEditorServices isn't a single binary β€” it's a PowerShell module bundle launched by a `pwsh` (PowerShell 7+) or `powershell` host. Setup: 1. Install [PowerShell](https://github.com/PowerShell/PowerShell) so `pwsh` (or Windows `powershell`) is on PATH. 2. Download the latest release zip from [PowerShellEditorServices releases](https://github.com/PowerShell/PowerShellEditorServices/releases) and extract it. 3. Point Hermes at the extracted bundle β€” the directory that contains `PowerShellEditorServices/Start-EditorServices.ps1`. Either: - set `lsp.servers.powershell.command: ["/path/to/bundle"]` in `config.yaml`, or - extract it to `/lsp/PowerShellEditorServices`, or - export `PSES_BUNDLE_PATH=/path/to/bundle`. If the bundle is missing, Hermes emits a one-time warning in the logs with the download link. A few servers are installed alongside a peer dependency that npm won't auto-pull. The current case is `typescript-language-server`, which requires the `typescript` SDK importable from the same `node_modules` tree β€” Hermes installs both packages together when auto-install fires on first use. ## Configuration The defaults work for typical setups; nothing to set if the binaries are on PATH. ```yaml # config.yaml lsp: # Master toggle. Disabling skips the entire subsystem β€” no servers spawn. enabled: true # How long to wait for diagnostics after each write. wait_mode: document # "document" or "full" # Max seconds to wait for the server to re-check the file after an # edit. Only *fresh* diagnostics (produced for the post-edit # content) are ever reported; if the server doesn't finish within # this budget, the edit reports "no LSP data" rather than stale # errors from before the edit. Raise this for slow servers on big # projects (tsserver, rust-analyzer mid-indexing). wait_timeout: 5.0 # How to handle missing server binaries. # auto β€” install via npm/pip/go install into /lsp/bin # manual β€” only use binaries already on PATH install_strategy: auto # How long an unused language-server client stays alive (seconds). # Idle servers are shut down automatically and respawned on the next # relevant file operation. Set to 0 to disable idle reaping and keep # servers alive for the life of the process. Values below 30s are # clamped to 30 so a sweep can never reap a client mid-operation. idle_timeout: 600 # Per-server overrides (all optional). servers: pyright: disabled: false command: ["/abs/path/to/pyright-langserver", "--stdio"] env: { PYRIGHT_LOG_LEVEL: "info" } initialization_options: python: analysis: typeCheckingMode: "strict" typescript: disabled: true # skip TS even when its extensions match ``` ### Per-server keys * `disabled: true` β€” skip this server entirely even when its extensions match a file. * `command: [bin, ...args]` β€” pin a custom binary path. Bypasses auto-install. * `env: {KEY: value}` β€” extra env vars passed to the spawned process. * `initialization_options: {...}` β€” merged into the LSP `initializationOptions` payload sent in the `initialize` handshake. Server-specific; consult the language server's docs. ## Installation locations When `install_strategy: auto`, Hermes installs binaries into `/lsp/bin/`. NPM packages land in `/lsp/node_modules/` with bin symlinks one level up. Go binaries come from `go install` with `GOBIN` pointed at the staging dir. Nothing is ever installed to `/usr/local/`, `~/.local/`, or any other shared location β€” the staging dir is fully Hermes-owned and is removed when you reset the profile. ## Performance characteristics LSP servers are **lazy-spawned** on first use. Editing a Python file in a project that's never seen `.py` traffic spawns pyright; the spawn takes 1-3 seconds for most servers (rust-analyzer can take 10+ on a cold project). Subsequent edits in the same workspace re-use the running server. The LSP layer adds a few milliseconds to clean writes when no diagnostics are emitted. When diagnostics are emitted, the wait budget is `wait_timeout` seconds β€” typically the server responds in tens of milliseconds for pyright/tsserver and a few seconds for rust-analyzer mid-indexing. Diagnostics are **freshness-gated**: a result only counts when the server produced it for the content of the current edit (a `publishDiagnostics` push at/after the change, or a pull request answered after it). Slow servers that haven't re-checked yet result in "no data" for that edit β€” never in yesterday's errors being re-reported as current. Servers are kept alive while they're being used and shut down after `lsp.idle_timeout` seconds (default 600) with no file activity β€” a long-running async service that touches many worktrees no longer accumulates one language-server process per workspace forever. A reaped server is respawned automatically on the next relevant file operation. Set `idle_timeout: 0` to disable reaping and hold every server's index warm for the life of the process. ## Disabling Set `lsp.enabled: false` in `config.yaml` to disable the entire subsystem. The post-write check falls back to the in-process syntax check (`ast.parse` for Python, `json.loads` for JSON, etc.) which ships unchanged from earlier versions. To disable a single language without disabling the whole layer: ```yaml lsp: servers: rust-analyzer: disabled: true ``` ## Troubleshooting **A server remains unavailable** The binary is not on PATH or in `/lsp/bin/`, and automatic installation either is disabled or failed. Install it through the language's normal toolchain, or set `lsp.servers..command` to its absolute command. Some servers are thin wrappers around another executable. The most common case is `bash-language-server`, which delegates diagnostics to `shellcheck`. Install the sidecar through your OS package manager: ``` apt install shellcheck # Debian / Ubuntu brew install shellcheck # macOS scoop install shellcheck # Windows ``` The missing backend is logged at server spawn time in `/logs/agent.log`. **Server starts but never returns diagnostics** Check `/logs/agent.log` for `[agent.lsp.client]` entries β€” both stderr from the language server and protocol errors land there. Some servers (rust-analyzer especially) need to finish a project-wide index before they emit per-file diagnostics; the first edit after server start may complete with no diagnostics, with subsequent edits picking them up. **Server crashed** A crashed server is added to the broken-set and will not be retried for the rest of that service lifetime. Close the owning `AIAgent` instances or `await shutdown_service()`; the next service lifetime can spawn it again. **Editing a file outside any git repo** By design, LSP only runs inside a git repository. If the project isn't yet initialized, run `git init` to enable LSP diagnostics. Otherwise the in-process syntax-only fallback applies. --- # MCP # MCP Model Context Protocol servers provide external tools without permanently growing the Hermes core schema. The retained client supports stdio, Streamable HTTP, and SSE transports. ## Lifecycle The first awaited agent boundary reads `mcp_servers` from `$HERMES_HOME/config.yaml`, connects to configured servers, discovers their catalogs, and refreshes the agent's tool snapshot. `await agent.close()` releases the associated subprocesses, HTTP sessions, keepalives, and registrations. ```python async with AIAgent( ..., enabled_toolsets=["mcp-database"], ) as agent: await agent.chat("Query the configured database server.") ``` A server named `database` receives the canonical toolset `mcp-database` and a raw-name alias. Tool names are normalized so collisions between servers cannot silently overwrite one another. ## Runtime behavior - MCP calls are awaited on the owning event loop. - Reconnection and timeout handling remain asynchronous. - Tool observations preserve model call order in history and trajectories. - A server can opt into parallel-safe calls; otherwise its operations remain serialized. - Include/exclude patterns can narrow a server catalog. - Elicitation is routed through the host clarification callback. Large MCP catalogs can participate in progressive tool search, avoiding a large schema prefix on every model request. ## Security boundary An stdio MCP definition executes a local command with the Python process's authority. A remote MCP server receives requests and can return content that the model will consume. Use pinned packages, restricted working directories, least-privilege tokens, TLS, catalog filters, and application approval for sensitive operations. Never embed tokens directly in a checked-in YAML file. Reference environment variables such as `${env:TEAM_MCP_TOKEN}`. Configuration examples and lifecycle usage are in [Use MCP with Hermes](../../guides/use-mcp-with-hermes.md). --- # Memory # Memory Memory stores curated facts across conversations. It is separate from session history: a session preserves a transcript, while memory keeps a small set of facts intended to remain useful beyond one transcript. ## Built-in stores The built-in provider uses two files: ```text $HERMES_HOME/memories/ β”œβ”€β”€ MEMORY.md # Agent notes, environment facts, and conventions └── USER.md # User preferences and profile information ``` Enable the prompt surfaces in `$HERMES_HOME/config.yaml`: ```yaml memory: memory_enabled: true user_profile_enabled: true memory_char_limit: 2200 user_char_limit: 1375 ``` Then expose the write/read tool to the model: ```python agent = AIAgent(..., enabled_toolsets=["memory"]) ``` `skip_memory=True` disables memory loading for an agent regardless of the shared configuration. The batch runner sets this deliberately so one dataset item cannot contaminate another. ## Cache-stable snapshots Memory and user-profile text are bounded and frozen into the conversation's system-prompt snapshot. A memory tool call can update the files, but it does not mutate the already cached prompt prefix halfway through a conversation. A later conversation sees the updated snapshot. ## External providers The retained plugin surface includes Mem0 and ByteRover integrations. The built-in provider remains available, and at most one external memory provider can be active at a time. Provider initialization, prefetch, turn sync, tool calls, session transitions, and shutdown use async contracts. Mem0's packaged SDK is optional (`mem0` install extra); self-hosted HTTP modes can use the base async HTTP client. Availability still depends on external credentials and services. ## Privacy and tenancy Memory is application state, not a model-provider privacy boundary. Do not store credentials or sensitive material that the model should not receive. Isolate `HERMES_HOME`, memory provider namespaces, and session databases between untrusted tenants. See [Security](../security.md). --- # Feature Overview # Feature Overview Async Hermes Agent is a library-focused conversion of the Hermes agent harness. It preserves the core behavior needed for tool-using inference and trajectory generation while moving the retained I/O path to native async APIs. ## Retained surfaces | Surface | What it provides | | --- | --- | | Agent loop | Interleaved reasoning, model calls, tool calls, observations, compression, and finalization | | Providers | Awaitable model transports and lazily discovered profiles, with documented SDK boundaries | | Tools | File, local terminal, web, browser, vision/media, planning, clarify, delegation, memory, and session search | | Skills | On-demand procedural instructions from local or shared directories | | MCP | Stdio and HTTP external tool servers with async lifecycle ownership | | Memory | Bounded file-backed memory plus optional external providers | | Sessions | Explicit awaitable SQLite persistence and resume helpers | | Training data | Ordered trajectories, batch generation, checkpoints, statistics, and compression utilities | Learn more in [Tools](./tools.md), [Skills](./skills.md), [MCP](./mcp.md), [Memory](./memory.md), [Browser automation](./browser.md), and [Batch processing](./batch-processing.md). ## Native-async contract The existing public names are coroutines: ```python result = await agent.run_conversation("Question") answer = await agent.chat("Follow-up") await agent.close() ``` The retained runtime directly awaits model, tool, MCP, subprocess, and database entry points. Unsupported synchronous provider and tool transports fail explicitly. Regular-file operations currently use executor-backed `aiofiles`, and the awaitable SQLite facade uses `aiosqlite`'s connection worker thread. Supported CPython versions expose no portable asyncio regular-file or embedded SQLite API. The package therefore guarantees directly awaitable, event-loop-nonblocking entry points, not zero-thread or OS-native persistence. Native async allows independent I/O-bound work to overlap; it is not a promise that every workload will use less CPU or memory. Provider latency, model cost, tool behavior, and host limits still dominate many deployments. ## Behavioral invariants The conversion preserves the Hermes narrow-waist contracts: - the system-prompt prefix remains stable during a conversation; - strict model-message alternation is maintained; - model tool-call order determines observation order, including parallel-safe execution; - saved trajectories preserve reasoning, calls, observations, and the final answer; - one agent's turns are serialized while independent agents may overlap; - cancellation cleans up child tasks and attempts partial persistence. ## Intentionally not included This package does not ship the upstream classic CLI, TUI, desktop app, dashboard, messaging platforms, scheduler, FastAPI application, or editor adapter. It also does not train a model. It provides the inference and training-data harness that a service or fine-tuning pipeline can embed. --- # Skills # Skills Skills are Markdown instruction packages that the model discovers and reads on demand. They are useful for repeatable workflows, project conventions, and specialized tool procedures. ## Layout ```text $HERMES_HOME/skills// β”œβ”€β”€ SKILL.md β”œβ”€β”€ references/ # Optional detailed documentation β”œβ”€β”€ templates/ # Optional reusable templates β”œβ”€β”€ scripts/ # Optional helper scripts └── assets/ # Optional assets ``` `SKILL.md` begins with YAML frontmatter: ```markdown --- name: release-review description: Verify a Python package before publishing a release. --- # Release review Run focused tests, inspect package contents, and report blockers before publish. ``` Use `skills.external_dirs` in `config.yaml` to discover shared skill roots: ```yaml skills: external_dirs: - /srv/team-skills ``` ## Tool surface Enable `enabled_toolsets=["skills"]` to expose: - `skills_list`, which scans local and external roots; - `skill_view`, which returns the complete selected document or supporting file; - `skill_manage`, which can create, patch, edit, delete, write, and remove skill files. Skill content is not paginated. The model must receive the complete selected instruction rather than reading only the first page. Repeated unchanged views within one task can be deduplicated without changing the earlier content. ## Prompt-cache behavior The list of available skill metadata can be incorporated into the stable conversation context, while full instructions are loaded through a tool call. Creating or editing a skill invalidates the skill snapshot for a later conversation; it does not rewrite past messages in the current conversation. ## Packaging and trust Do not assume the Python distribution installs a large upstream skill catalog. Provision the exact skills needed by the application and version them alongside the deployment when reproducible trajectories matter. Skills influence a tool-capable model, so treat third-party skill text and scripts as trusted code. Filesystem permissions remain the strongest way to make a shared skill repository read-only. For a practical walkthrough, see [Work with Skills](../../guides/work-with-skills.md). --- # Tools and Toolsets # Tools and Toolsets Tools are structured functions the model can call. Toolsets group related tools so each agent exposes only the capability surface required by its task. ## Select toolsets per agent ```python async with AIAgent( ..., enabled_toolsets=["file", "terminal", "web", "skills"], disabled_toolsets=["browser"], ) as agent: result = await agent.run_conversation("Investigate and patch the issue.") ``` An explicit allowlist is recommended for services and data generation. The historical full-toolset name `hermes-cli` is retained for compatibility, but it does not mean this distribution ships a CLI. ## Retained built-in groups | Toolset | Main tools | | --- | --- | | `file` | `read_file`, `write_file`, `patch`, `search_files` | | `terminal` | `terminal`, `process` using the local async subprocess backend | | `web` / `search` | `web_search`, optionally `web_extract` | | `browser` | navigate, snapshot, click, type, scroll, keyboard, CDP, console, dialogs, and browser vision | | `skills` | `skills_list`, `skill_view`, `skill_manage` | | `memory` | bounded persistent memory operations | | `session_search` | search, browse, and read an attached session store | | `vision` / `image_gen` | image analysis and provider-backed image generation | | `todo` | durable planning state across context compression | | `clarify` | request host/user clarification | | `delegation` | run isolated child-agent subtasks | Provider plugins can add web, browser, image, video, memory, or other optional tools. MCP servers receive their own dynamic toolsets. ## Scheduling semantics Native async handlers are awaited directly. Independent parallel-safe tool calls may execute concurrently, but results are appended to model history in the order the model emitted the calls. Sequential tools, interactive approval boundaries, budget checks, guardrails, and steering barriers retain their ordered semantics. There is no fallback that invokes a synchronous handler in a worker thread. A sync-only tool is rejected during async initialization or dispatch. ## Progressive discovery Large optional plugin and MCP catalogs can be exposed through tool search so every schema is not sent with every request. Fundamental tools in the core surface are never deferred. This keeps the prompt-cache prefix and core schema stable while still allowing edge capabilities to be discovered. ## Host interaction Some tools require application decisions. For example, MCP elicitation and the `clarify` tool use `clarify_callback`; terminal and other sensitive operations may require host authorization policy. A headless service should fail clearly when it has no valid interaction boundary rather than trying to read terminal input. Tool access is security authority. Review [Security](../security.md) before enabling terminal, file-write, browser, third-party skill, or MCP capabilities. --- # Security # Security An agent can execute commands, modify files, browse sites, and call external services. Treat every enabled tool as real authority granted to model output. ## Use least-privilege toolsets Give each workload only the toolsets it needs: ```python agent = AIAgent( ..., enabled_toolsets=["web", "file"], disabled_toolsets=["terminal", "browser"], ) ``` The local terminal backend runs processes with the permissions of the Python process. This distribution does not ship Docker, SSH, Modal, Daytona, or Singularity isolation backends. Use operating-system, container, or service isolation outside the library when commands must not reach the host. ## Protect credentials - Store API keys in the process environment or `$HERMES_HOME/.env`. - Keep non-secret behavior in `config.yaml`. - Use scoped, revocable tokens for MCP and cloud providers. - Never place secrets in prompts, skills, trajectories, logs, or source control. - Redact tool observations before returning them to untrusted clients. ## Review skills and MCP servers Skills are executable instructions in the practical sense: they influence a model that can use tools. Review third-party skill text and supporting scripts before exposing them. An stdio MCP entry starts the configured command locally. An HTTP MCP server can return untrusted content and request tool actions or elicitation. Pin server packages, restrict catalog tools with include/exclude patterns, use least-privilege credentials, and require host authorization for sensitive operations. See [MCP](./features/mcp.md) and [Skills](./features/skills.md). ## Network boundaries Browser and web tools guard private, loopback, link-local, and cloud-metadata targets. Keep the default: ```yaml security: allow_private_urls: false ``` Setting it to `true` expands SSRF reach for every caller using that Hermes home. If access to a private application is intentional, prefer a separately isolated agent with narrow credentials and network policy. Browser-specific behavior is documented in [Browser automation](./features/browser.md). ## Service responsibilities The library does not provide an authenticated HTTP API. A FastAPI or other host must implement authentication, tenant isolation, rate limits, request size limits, timeouts, audit policy, and mapping between users and ordered agent instances. Do not share one memory directory, session ID, or mutable agent instance across untrusted tenants. Use a separate `HERMES_HOME`, database, and lifecycle where the trust boundary requires it. ## Shutdown and cancellation Use the async context manager or `await agent.close()` so subprocesses, MCP sessions, provider clients, and child tasks are released. On external cancellation, the runtime performs partial persistence and then re-raises `CancelledError`; callers must still enforce their own timeout and retry policy. --- # Sessions # Sessions An `AIAgent` keeps its live conversation history across sequential calls. To persist that history across agent instances or processes, attach a `SessionDB` explicitly. ## In-memory lifecycle ```python from run_agent import AIAgent async def in_memory_conversation(): async with AIAgent(...) as agent: await agent.chat("My project uses Python 3.12.") return await agent.chat("Which Python version did I mention?") ``` One instance represents one ordered conversation. Concurrent turns on that instance are serialized; separate instances can run concurrently. ## Enable durable storage ```python from hermes_state import SessionDB from run_agent import AIAgent async def stored_conversation(): db = SessionDB("./state.db") async with AIAgent( ..., session_db=db, session_id="project-review", ) as agent: return await agent.run_conversation("Review this project.") ``` `SessionDB` uses `aiosqlite`. Its constructor is state-only; the connection and schema initialize on the first awaited operation. Passing it explicitly starts durable transcript persistence at the turn prologue and selects the database path. A recall tool may otherwise open the default `$HERMES_HOME/state.db` lazily. An injected store is borrowed by the agent; the host that created it closes it once during worker shutdown. Only a lazily created default store is owned by the agent. `SessionDB.close()` is idempotent. ## Use PostgreSQL for a service SQLite remains the default and is a good fit for a single-process application. For a service whose workers share a durable store, install the optional PostgreSQL backend and inject it through the same existing `session_db=` argument: ```bash uv sync --extra postgres ``` ```python from hermes_state_postgres import SessionDB from run_agent import AIAgent async def postgres_conversation(): db = SessionDB( "postgresql+asyncpg://user:password@db.example:5432/hermes", ) try: async with AIAgent( ..., session_db=db, session_id="project-review", ) as agent: return await agent.run_conversation("Review this project.") finally: await db.close() ``` The PostgreSQL `SessionDB` keeps the SQLite method names and awaited calling style; only the import and explicit DSN change. A store reads the active profile's `database.postgres` pool and driver settings once when its first database operation initializes the engine. Create a new store after changing those settings. See [SessionDB storage and PostgreSQL settings](../developer-guide/session-storage.md) for the supported options. Writable initialization creates or additively reconciles the retained tables and indexes; read-only initialization never migrates an existing database. For a read-only endpoint, pass `read_only=True`: ```python readonly_db = SessionDB(read_replica_url, read_only=True) ``` This blocks SessionDB writes and enables PostgreSQL transaction read-only mode; it does not choose a replica automatically. In a multi-worker service, create and close one store per worker lifespan and share it with that worker's agents. Plan for a possible connection count of `workers * (pool_size + max_overflow)`. Other retained stores, such as memory plugin databases, remain separate from the core PostgreSQL SessionDB. ## Resume in a new agent Passing only the old `session_id` does not automatically load its messages. Resolve the current compression descendant, load model history, and provide it to `run_conversation()`: ```python from hermes_state import SessionDB from run_agent import AIAgent async def resume_conversation(): db = SessionDB("./state.db") tip = await db.resolve_resume_session_id("project-review") model_history, display_history = await db.get_resume_conversations(tip) async with AIAgent( ..., session_db=db, session_id=tip, ) as agent: result = await agent.run_conversation( "Continue from the saved review.", conversation_history=model_history, ) return result, display_history ``` `model_history` is the alternation-repaired history fed to the model. `display_history` includes the full ancestor-to-tip lineage for applications that render a transcript. ## Search and compression The store retains message order, structured tool calls, reasoning, display metadata, and compression lineage. Its FTS-backed search powers the optional `session_search` toolset: ```python async def search_prior_sessions(): async with AIAgent( ..., session_db=SessionDB("./state.db"), enabled_toolsets=["session_search"], ) as agent: return await agent.chat("Find the earlier deployment discussion.") ``` Context compression can end one database session and continue in a linked child session. Always call `resolve_resume_session_id()` before a cross-process resume so post-compression messages are not missed. Cancellation of an active conversation attempts a crash-safe partial persist before propagating `CancelledError`. This protects recoverability but does not replace application-level backups or SQLite filesystem durability planning. --- # Integrations # Integrations The agent core is intentionally narrow. Capabilities are attached at its async edges so the conversation loop, prompt-cache prefix, message alternation, and trajectory order remain stable. ## Integration surfaces | Surface | Best for | Lifecycle | | --- | --- | --- | | Model provider profiles | Selecting an inference endpoint and wire protocol | Initialized and closed by `AIAgent` | | Provider plugins | Web search, browser, image/video, and external memory backends | Discovered lazily at an awaited boundary | | MCP | External structured tools owned by another process or service | Connected, called, and closed asynchronously | | Skills | Reusable instructions and supporting files | Discovered and read from disk on demand | | Built-in tools | Fundamental file, terminal, memory, browser, and planning operations | Scheduled by the core tool executor | Start with [Model providers](./providers.md). For external tools, see [MCP](../user-guide/features/mcp.md); for instructional extensions, see [Skills](../user-guide/features/skills.md). ## Plugin discovery Bundled provider definitions live under the installed `plugins/` package. Application-owned plugins can live under `$HERMES_HOME/plugins/`. Discovery is lazy, and a user provider profile with the same name can override its bundled counterpart. A plugin must implement the native-async contract for its category. A synchronous handler is rejected rather than hidden behind a thread bridge. Provider-specific dependencies should remain optional and fail with a clear installation hint when absent. ## What is not an integration surface here This distribution does not ship the upstream CLI/TUI, desktop or dashboard, messaging platforms, cron scheduler, FastAPI server, or editor adapter. Build those applications around the library API rather than depending on residual helper package names. The framework-neutral embedding contract is documented in the [Python library guide](../guides/python-library.md). --- # Model Providers # Model Providers `AIAgent` separates a provider profile from the model identifier. A profile resolves credentials, base URL, API mode, and transport behavior; `model` names the model or deployment exposed by that provider. ## Prefer explicit construction Explicit arguments are easiest to audit in services and tests: ```python import os from run_agent import AIAgent agent = AIAgent( provider=os.environ["MODEL_PROVIDER"], base_url=os.getenv("MODEL_BASE_URL") or None, api_key=os.getenv("MODEL_API_KEY") or None, model=os.environ["MODEL_ID"], ) ``` `MODEL_PROVIDER`, `MODEL_ID`, `MODEL_API_KEY`, and `MODEL_BASE_URL` in this example belong to the host application. They let one example cover every provider without presenting one vendor as the default. Do not assume that a provider's current free models, aliases, context windows, or prices are stable. Supply a model that supports the tool-calling and reasoning behavior required by your application. ## Bundled provider profiles The provider registry ships the following profiles. Credential names come from the profile source; OAuth and cloud-identity routes may not use a static API-key variable. | Profile | Credential or identity | Transport family | | --- | --- | --- | | `anthropic` | `ANTHROPIC_API_KEY`, `ANTHROPIC_TOKEN`, or Claude OAuth | Native Anthropic | | `gemini` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | Native Gemini HTTP | | `vertex` | Google Application Default Credentials | Google Vertex | | `bedrock` | AWS SDK credential chain | AWS Bedrock Converse (SDK bootstrap limitation below) | | `azure-foundry` | `AZURE_FOUNDRY_API_KEY` and `AZURE_FOUNDRY_BASE_URL`, or restricted Entra ID | OpenAI-compatible / Azure identity | | `openai-codex` | ChatGPT/Codex OAuth state | Codex Responses | | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN` | GitHub Copilot | | `nous` | `NOUS_API_KEY` or Nous OAuth state | OpenAI-compatible | | `openrouter` | `OPENROUTER_API_KEY` | OpenAI-compatible | | `deepseek` | `DEEPSEEK_API_KEY` | OpenAI-compatible | | `xai` | `XAI_API_KEY` or xAI OAuth state | OpenAI-compatible / Responses | | `zai` | `GLM_API_KEY`, `ZAI_API_KEY`, or `Z_AI_API_KEY` | OpenAI-compatible | | `kimi-coding` | `KIMI_API_KEY` or `KIMI_CODING_API_KEY` | OpenAI-compatible | | `minimax` | `MINIMAX_API_KEY`; OAuth uses `minimax-oauth` | Anthropic-compatible | | `alibaba` | `DASHSCOPE_API_KEY` | OpenAI-compatible | | `huggingface` | `HF_TOKEN` | OpenAI-compatible | | `fireworks` | `FIREWORKS_API_KEY` | OpenAI-compatible | | `nvidia` | `NVIDIA_API_KEY` | OpenAI-compatible | | `custom` | Host-defined key and base URL | OpenAI-compatible custom/local | Additional bundled profiles include AI Gateway, Arcee, DeepInfra, GMI, KiloCode, Novita, Ollama Cloud, OpenCode, Qwen OAuth, StepFun, Upstage, and Xiaomi. The source of truth is `plugins/model-providers/`; this page groups routes by contract instead of ranking vendors. Applications that inspect the registry directly use the retained upstream names with an awaited first-use discovery boundary: ```python from providers import get_provider_profile, list_providers profile = await get_provider_profile("openrouter") profiles = await list_providers() ``` Both calls perform native-async plugin discovery when needed. There is no synchronous discovery fallback; subsequent calls reuse the in-memory registry. ## Retained transport families The retained runtime exposes awaited paths for these families. Their network transports are native async except where the SDK boundary below says otherwise: | Family | Typical profiles | Dependency | | --- | --- | --- | | OpenAI-compatible chat/responses | OpenRouter, custom/local endpoints, DeepSeek, xAI and other compatible gateways | Base install | | Native Anthropic | `anthropic` | `anthropic` extra | | Google Gemini HTTP | `gemini` | Base install | | Google Vertex | `vertex` | `vertex` extra for credentials | | Microsoft Foundry/Azure | `azure-foundry` | Base transport; restricted `azure-identity` extra for Entra ID | | AWS Bedrock | `bedrock` | `bedrock` extra; see SDK boundary below | | Codex Responses and Copilot ACP | Corresponding bundled profiles | Profile-specific credentials/runtime | Bundled profile discovery includes additional OpenAI-compatible services. A profile in the source tree means Hermes knows how to resolve that service; it does not guarantee that an external account, endpoint, model, or optional SDK is currently available. ## Install an optional transport From a source checkout: ```bash uv sync --extra anthropic uv sync --extra vertex uv sync --extra azure-identity uv sync --extra bedrock ``` See [Installation](../getting-started/installation.md) for the complete extras list. ### Azure Identity and Bedrock SDK boundaries The pinned `azure-identity` asynchronous package does not expose the same credential chain as its synchronous `DefaultAzureCredential`: broker and interactive-browser entries are absent, while shared token cache, Visual Studio Code, and certificate paths still perform synchronous file/cache work. Async Hermes therefore enables the verified client-secret environment or managed- identity route and fails clearly when an unsupported chain is selected. When `AZURE_FEDERATED_TOKEN_FILE` configures projected Workload Identity (or `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` explicitly selects it), Async Hermes uses a bounded adapter around the public async `ClientAssertionCredential`: it reads the projected file only on first use and after the 600-second refresh window, with a 64 KiB limit and a one-second read timeout. It does not support the Kubernetes token-proxy/identity-binding variables. Static Azure Foundry API-key authentication is unaffected. The pinned `aiobotocore` transport provides coroutine network requests, but client/credential construction still synchronously loads botocore config and service-model files. AWS profile, SSO, web-identity, and related file-backed credential chains therefore do not satisfy this project's strict zero-thread, OS-native bootstrap ideal. This is a documented SDK boundary rather than a hidden thread fallback in Hermes. A single-profile process may still use the SDK's default chain with that bootstrap limitation. When profile multiplexing is active, Hermes accepts explicit profile-scoped AWS credentials or Bedrock bearer authentication and fails explicitly for shared/global credential chains that cannot be isolated safely. ## Custom OpenAI-compatible endpoint ```python agent = AIAgent( provider="custom", base_url="http://127.0.0.1:8000/v1", api_key="local-or-required-key", model="your-served-model", ) ``` The endpoint must implement the selected OpenAI-compatible API and support the message/tool schema used by your workload. Running a local model server is outside this package. ## Configuration-based selection For applications that prefer file configuration, use non-secret settings in `$HERMES_HOME/config.yaml`: ```yaml model: provider: "" default: "" # base_url: "" ``` Keep the credential in `$HERMES_HOME/.env` or the process environment: ```dotenv =... ``` Explicit constructor arguments take priority for that agent instance. Details are in [Configuring models](../user-guide/configuring-models.md). ## Failure behavior Provider setup and requests are awaited. If a selected API mode has no native async transport, initialization fails explicitly; the runtime does not call a synchronous SDK through `asyncio.to_thread()`. Always close an initialized provider with `await agent.close()` or an async context manager. --- # Python Library # Python Library Async Hermes Agent is designed to be embedded. It ships the agent harness, not an application server or user interface. ## Core interface ```python from run_agent import AIAgent async def chat_once(): agent = AIAgent(...) try: result = await agent.run_conversation("Question") answer = await agent.chat("Follow-up question") return result, answer finally: await agent.close() ``` The method names and argument shapes remain at their upstream locations. The methods are coroutines; calls without `await` only create coroutine objects. Prefer the context manager: ```python async def complete_task(): async with AIAgent(...) as agent: return await agent.run_conversation("Complete this task") ``` Entering initializes the selected provider, discovers plugins, and establishes configured MCP lifecycles. Exiting closes model clients, MCP sessions, child tasks, memory providers, and an attached session database. `close()` is idempotent. ## Conversation results The stable result surface includes: ```python result["final_response"] result["messages"] result["completed"] ``` Normal completed turns can also include `session_id`, provider/model routing, token and cost fields, API-call counts, reasoning, and turn-exit metadata. Early terminal/error results and providers that do not report a usage field may omit them, so consumers should use `.get()` for optional metadata and accounting data. `chat()` is a convenience interface that returns only `final_response`. ## Conversation ownership One agent represents one ordered conversation. Its turn lock serializes concurrent calls: ```python import asyncio async def ordered_turns(agent): # Safe, but these turns run in submission order rather than in parallel. return await asyncio.gather( agent.chat("First turn"), agent.chat("Second turn"), ) ``` For independent work, allocate independent agents: ```python async def independent_turns(): async with AIAgent(...) as a, AIAgent(...) as b: return await asyncio.gather( a.chat("Independent task A"), b.chat("Independent task B"), ) ``` ## Cancellation Cancelling an active turn cancels its child work, persists a partial session when a `SessionDB` is attached, and re-raises `asyncio.CancelledError`: ```python async def cancel_turn(agent): task = asyncio.create_task(agent.run_conversation("Long task")) task.cancel() try: await task except asyncio.CancelledError: pass ``` Hermes-internal interrupts are different: they return a partial result dict so the caller can display or inspect the work completed so far. ## Explicit durable sessions For a chosen database path and persistence beginning at the turn prologue, construct and pass `SessionDB` explicitly: ```python from hermes_state import SessionDB from run_agent import AIAgent async def durable_turn(): db = SessionDB("./state.db") async with AIAgent(..., session_db=db, session_id="customer-42") as agent: return await agent.chat("Remember this conversation") ``` The database connection and schema initialize on the first awaited operation. Ordinary turns without an injected store do not persist a transcript. A recall tool that needs session storage can lazily open the default `$HERMES_HOME/state.db`; the agent then owns and closes that handle. The agent also owns and closes an explicitly supplied database at shutdown. See [Sessions](../user-guide/sessions.md) for explicit resume handling. ## Tools, skills, MCP, and memory Select only the tool groups a conversation needs. Availability checks still remove a tool when its credential, executable, callback, or backend is absent: ```python async def work_with_extensions(): async with AIAgent( ..., enabled_toolsets=["file", "skills", "memory", "mcp-project"], ) as agent: return await agent.chat("Read the project skill and continue the task.") ``` This example assumes a configured MCP server named `project`; its canonical toolset is `mcp-project`. Put skills below `$HERMES_HOME/skills//SKILL.md` or configure `skills.external_dirs`. Configure MCP servers under `mcp_servers` and memory under `memory` in `$HERMES_HOME/config.yaml`. The first awaited agent boundary loads skills/plugins and discovers MCP tools. Agent shutdown closes the MCP lease and the selected memory provider. The built-in file-backed memory exposes the `memory` tool. An external memory provider is selected with `memory.provider`; install that provider's optional extra when its package metadata requires one. Skills and memory are mutable application state, so isolate `HERMES_HOME` between untrusted tenants. See [Skills](../user-guide/features/skills.md), [Memory](../user-guide/features/memory.md), and [MCP Configuration](../reference/mcp-config-reference.md). ## Service integration No FastAPI application is bundled. A host framework should own startup, shutdown, authentication, quotas, and request routing while awaiting the library directly: ```python from fastapi import FastAPI from run_agent import AIAgent app = FastAPI() @app.post("/chat") async def chat(message: str): # Isolated-job example. A production chat service should keep one agent # per conversation ID and close it when that conversation expires. async with AIAgent(...) as agent: return await agent.run_conversation(message) ``` FastAPI is intentionally a downstream example, not a dependency. Sharing one global instance would both mix unrelated mutable conversation state and serialize every request. --- # Use MCP with Hermes # Use MCP with Hermes The Model Context Protocol (MCP) connects external tool servers to the agent without adding their schemas to the core. MCP discovery, calls, reconnection, and shutdown are awaited on the agent event loop. ## Configure a stdio server Add a server under `mcp_servers` in `$HERMES_HOME/config.yaml`. `HERMES_HOME` defaults to `~/.hermes`. ```yaml mcp_servers: filesystem: command: npx args: - -y - "@modelcontextprotocol/server-filesystem" - /workspace connect_timeout: 60 timeout: 300 ``` The configured command is started as a local subprocess. Install its runtime and package manager separately; this Python library does not install Node.js or third-party MCP servers. ## Configure an HTTP server ```yaml mcp_servers: knowledge: url: "https://mcp.example.com/mcp" headers: Authorization: "Bearer ${env:KNOWLEDGE_MCP_TOKEN}" connect_timeout: 30 timeout: 180 ``` Keep the token in the process environment or `$HERMES_HOME/.env`: ```dotenv KNOWLEDGE_MCP_TOKEN=... ``` Streamable HTTP is selected for `url` entries by default. Set `transport: sse` only when the server implements the older SSE transport. ## Expose the server tools to an agent At the first awaited lifecycle boundary, Hermes discovers configured servers and registers each catalog under `mcp-` (with the raw server name as an alias). Use that toolset in the agent allowlist: ```python from run_agent import AIAgent async with AIAgent( ..., enabled_toolsets=["file", "mcp-filesystem"], ) as agent: result = await agent.run_conversation( "List the files exposed by the filesystem server." ) ``` Large optional catalogs may be exposed through progressive tool search, while core tools remain directly available. Tool observations appear in the normal message sequence and in saved trajectories. ## Limit a server catalog Use include or exclude patterns when a server exposes more tools than a task needs: ```yaml mcp_servers: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_PERSONAL_ACCESS_TOKEN: "${env:GITHUB_TOKEN}" tools: include: ["search_*", "get_*"] exclude: ["delete_*"] ``` Filtering reduces the visible capability surface; it is not a substitute for least-privilege credentials or server-side authorization. ## Elicitation and shutdown MCP servers can request user input through elicitation. Supply an async-capable host callback through `clarify_callback` when the selected servers use it. The agent converts approval-style elicitation into the same clarification boundary used by the built-in `clarify` tool. Always use `async with AIAgent(...)` or call `await agent.close()`. Shutdown releases stdio subprocesses, HTTP sessions, keepalive tasks, and registered MCP lifecycle state. See [MCP concepts](../user-guide/features/mcp.md) and [Security](../user-guide/security.md) before connecting an untrusted server. --- # Work with Skills # Work with Skills A skill is a directory whose `SKILL.md` gives the model reusable instructions. Skills are loaded on demand through tools, so a large skill library does not need to be copied into every model request. ## Create a skill Create a directory under `$HERMES_HOME/skills`; `HERMES_HOME` defaults to `~/.hermes`. ```text ~/.hermes/skills/ └── review-python-change/ β”œβ”€β”€ SKILL.md └── references/ └── checklist.md ``` Use YAML frontmatter with a concise activation description: ```markdown --- name: review-python-change description: Review a Python change for correctness, async safety, and tests. --- # Review a Python change 1. Read the changed code and its callers. 2. Check coroutine and cancellation behavior. 3. Run the focused tests before reporting findings. ``` The directory name and frontmatter name should be stable, lowercase identifiers. Put large reference material under `references/`, templates under `templates/`, scripts under `scripts/`, and other assets under `assets/`. `skill_view` can load those linked files when needed. ## Add shared skill directories Point the library at existing repositories without copying them: ```yaml skills: external_dirs: - ~/.agents/skills - /srv/team-skills ``` Relative entries resolve from `HERMES_HOME`; `~` and environment variables are expanded. Missing and duplicate directories are ignored. The local skills directory takes precedence during discovery. External directories are externally owned. Discovery and reading are safe by default, while an explicit foreground `skill_manage` call may edit an existing external skill in place. Use filesystem permissions if those repositories must remain immutable. ## Enable skill tools ```python async with AIAgent(..., enabled_toolsets=["skills", "file"]) as agent: result = await agent.run_conversation( "Find and follow the review-python-change skill for this patch." ) ``` The `skills` toolset contains: | Tool | Purpose | | --- | --- | | `skills_list` | Discover local and configured external skills | | `skill_view` | Read a complete `SKILL.md` or one of its supporting files | | `skill_manage` | Create, patch, edit, delete, or manage supporting files | Skill reads and writes expose awaited coroutine APIs. They currently use the package's executor-backed `aiofiles` regular-file layer, so they must not be described as OS-native file I/O. When trajectory saving is enabled, list/view/manage calls and their observations retain their normal place in the trajectory. ## Distribution note Source checkouts can contain example skills, but the installed library should not be treated as a comprehensive bundled skill catalog or installer. Deploy the skills your application intends to expose and version them separately when reproducibility matters. Skills are trusted procedural instructions with access to the agent's enabled tools. Review third-party skills before activation. See [Skills concepts](../user-guide/features/skills.md) and [Security](../user-guide/security.md). --- # Agent Loop Internals # Agent Loop Internals The public loop is `AIAgent.run_conversation()` in `run_agent.py`. Its detailed implementation remains in `agent/conversation_loop.py`, with turn setup and finalization split into focused modules at the same layer. ## Public interfaces ```python result = await agent.run_conversation( "Investigate the failure", system_message=None, conversation_history=None, task_id=None, ) answer = await agent.chat("Summarize the result") await agent.close() ``` `run_conversation()` returns the full result dictionary used by upstream library integrations. Important fields include `final_response`, `messages`, `completed`, and `api_calls`; recovery paths may also report partial or cleanup metadata. `chat()` returns only `result["final_response"]`. The implementation preserves these names rather than adding `arun_*` aliases or synchronous wrappers. ## Lifecycle ### Construction `AIAgent.__init__()` stores state and configuration. It does not open network, database, or MCP connections. ### Lazy initialization `async with AIAgent(...)` initializes provider, plugin, and MCP state before returning the agent. Calling `run_conversation()` directly also reaches the same awaited initialization path. ### Close `await agent.close()` is idempotent. It stops owned child tasks, waits for their termination, releases MCP ownership, and closes owned clients and session storage. Prefer the async context manager so exceptions cannot skip cleanup. ## One turn ### 1. Serialize the instance `run_conversation()` acquires a per-agent `asyncio.Lock`. Two callers sharing one agent cannot interleave mutations to its prompt cache or transcript. They wait in arrival order determined by the event loop. Separate agents have independent locks. ### 2. Build turn context `await build_turn_context()` in `agent/turn_context.py` performs the once-per- turn prologue: - normalize the user message and restore or build the system prompt; - resume or create the durable session when a `SessionDB` is attached; - hydrate turn-scoped state such as todos and interrupt tracking; - prefetch configured memory and refresh the MCP tool snapshot; - run the pre-model plugin hook; - estimate context pressure and compress when required; - persist the initial user turn before the first provider request. Pure message transformations remain synchronous helpers inside this awaited workflow. ### 3. Call the provider The selected transport is awaited directly. Provider adapters translate the OpenAI-shaped internal conversation into the provider's wire format and back without changing the stored message model. Streaming callbacks may receive text deltas, but the host still awaits one complete turn result. ### 4. Execute tools When the assistant returns tool calls, `agent/tool_executor.py` divides the batch at ordering and interaction barriers. - Async handlers are awaited directly. - Independent, parallel-safe calls may execute in a `TaskGroup`. - Sequential calls preserve their barriers. - Results are appended in the model's original call order. - Safety checks, budgets, checkpointing, middleware, and callbacks remain in the dispatch path. The completed assistant/tool sequence is persisted before the next provider iteration. ### 5. Continue or finish The loop repeats until it receives a final text response, an internal interrupt, an error path, or an iteration-budget exit. If the normal budget is exhausted without a final answer, the existing one-call, tool-free summary path is used when eligible. ### 6. Finalize `await finalize_turn()` in `agent/turn_finalizer.py`: - closes incomplete tool sequences when necessary; - saves a trajectory when enabled; - cleans up task-scoped browser and process resources; - persists the final transcript and session metadata; - returns the result dictionary even when a non-critical cleanup surface fails, recording cleanup errors instead of discarding a valid answer. ## Message invariants Messages use the familiar `system`, `user`, `assistant`, and `tool` roles. Reasoning is retained on assistant messages separately from visible content. The loop maintains provider-valid assistant tool calls followed by matching tool observations and avoids orphaned tool results. The cached system prompt is not rebuilt mid-conversation. Compression is the intentional mechanism that can replace older context while maintaining a stable resumed conversation. ## Cancellation and interrupts External task cancellation and Hermes' internal interrupt have different contracts: - On `asyncio.CancelledError`, the loop shields the short finalization needed to persist partial session and trajectory state, then re-raises cancellation to the host. - An internal interrupt stops at a safe boundary and returns the established partial result shape. Provider, tool, compression, and persistence awaits remain cancellation points. Cleanup tasks are awaited so they do not leak into the host loop. ## Synchronous fallback audit The retained provider and tool transports do not call `asyncio.to_thread()`, `run_in_executor()`, `run_until_complete()`, or blocking future `.result()` to hide a synchronous SDK. A retained provider or tool without a supported native-async path fails explicitly. The persistence layer has a documented implementation boundary: `aiofiles` implements regular-file operations through an executor, and `aiosqlite` queues embedded SQLite work to a connection thread. CPython has no portable asyncio API for regular files, metadata, durability operations, or embedded SQLite. These operations remain directly awaitable and do not block the host event loop, but zero-thread persistence is not a package guarantee. This rule applies to I/O. Keeping a short deterministic calculation as a normal `def` is expected and avoids coroutine overhead with no concurrency benefit. ## Related documentation - [Architecture](./architecture.md) - [Session Storage](./session-storage.md) - [Trajectory Format](./trajectory-format.md) - [Tools and Toolsets](/user-guide/features/tools) --- # Architecture # Architecture Async Hermes Agent is a library-focused derivative of Hermes Agent `v2026.8.13`. It keeps upstream file locations and public names where possible, then converts I/O-bearing runtime boundaries to coroutines. This makes upstream changes easier to compare without maintaining a parallel async module tree. ## System overview ```mermaid flowchart TD Host["Host application"] --> Agent["AIAgent β€” run_agent.py"] Dataset["Dataset"] --> Batch["BatchRunner β€” batch_runner.py"] Batch --> Agent Agent --> Turn["Turn prologue and conversation loop"] Turn --> Provider["Provider transports"] Turn --> Scheduler["Tool scheduler"] Turn --> Context["Prompt, memory, and compression"] Turn --> State["SessionDB β€” SQLite and FTS"] Turn --> Trajectory["Trajectory JSONL"] Scheduler --> Builtins["Built-in tools"] Scheduler --> MCP["MCP servers"] Scheduler --> Skills["Skills"] Scheduler --> Plugins["Capability plugins"] ``` The host owns the event loop and service boundary. There is no bundled HTTP server, interactive UI, messaging gateway, or scheduler in this distribution. ## Primary entry points ### `AIAgent` `run_agent.py` exports `AIAgent`, the stateful agent runtime. Construction is synchronous because it records configuration and creates no external connection. The external lifecycle starts at the first awaited operation: ```python from run_agent import AIAgent async with AIAgent(...) as agent: result = await agent.run_conversation("Review this change") ``` `__aenter__()` resolves the provider runtime, discovers plugins, and starts MCP discovery. `run_conversation()` also performs lazy initialization, so explicit context-manager use is recommended but not required. `close()` releases MCP, provider, database, browser, and child-task resources and is idempotent. ### `BatchRunner` `batch_runner.py` consumes JSONL prompts and runs bounded concurrent agent turns. Each prompt receives an isolated agent instance. Batch shards, checkpoints, statistics, and a merged trajectory file are written with async file operations. See [Trajectory Format](./trajectory-format.md). ## Code map | Path | Responsibility | | --- | --- | | `run_agent.py` | `AIAgent` public API, lifecycle, and compatibility surface | | `agent/conversation_loop.py` | Model/tool iteration and provider recovery | | `agent/turn_context.py` | Once-per-turn setup, prompt restore/build, prefetch, and initial persistence | | `agent/tool_executor.py` | Sequential and parallel-safe tool scheduling | | `agent/turn_finalizer.py` | Result construction, trajectory save, cleanup, and final persistence | | `agent/prompt_builder.py` | Stable system-prompt assembly | | `agent/context_compressor.py` | Context pressure handling and summarization | | `agent/transports/` | Native async model transports | | `model_tools.py` | Tool discovery, schemas, and dispatch entry points | | `tools/registry.py` | Tool registration and availability checks | | `tools/mcp_tool.py` | MCP discovery, calls, reconnection, and teardown | | `toolsets.py` | Static and dynamic tool group resolution | | `hermes_state.py` | `SessionDB`, SQLite persistence, FTS search, and maintenance | | `agent/trajectory.py` | Per-turn trajectory serialization | | `batch_runner.py` | Concurrent dataset execution and checkpointing | The reduced `hermes_cli/`, `gateway/`, `plugins/`, and `providers/` packages contain retained configuration, context, and extension contracts used by the library. They do not constitute the removed product applications. ## Turn data flow ```mermaid sequenceDiagram participant H as Host participant A as AIAgent participant S as SessionDB participant P as Provider participant T as Tool scheduler H->>A: await run_conversation(message) A->>A: build_turn_context() A->>S: await initial persistence loop until a final response or budget exit A->>P: await model request alt tool calls A->>T: await tool batch T-->>A: ordered observations A->>S: await incremental persistence else text response P-->>A: final content end end A->>A: await finalize_turn() A->>S: await final persistence A-->>H: result dictionary ``` ## Concurrency model - A per-instance `asyncio.Lock` serializes turns on one `AIAgent`. This keeps mutable history, prompt-cache state, and persistence ordered. - Different agent instances can make progress concurrently on the same event loop. - Parallel-safe tool calls run in an `asyncio.TaskGroup`; observations are appended in the model's original tool-call order. - Tools requiring interaction, ordering, or a safety barrier remain sequential. - CPU-only parsing, token estimates, schema normalization, and message transformations remain synchronous. Native async means active I/O is awaited. It is a concurrency contract, not a claim that every helper is a coroutine or that CPU work becomes cheaper. ## Behavior-preservation contracts The async conversion keeps these upstream invariants: - The system-prompt prefix remains stable for the life of a conversation. - Provider message alternation and tool-call/tool-result pairing remain valid. - Tool observations retain model-issued order even when execution overlaps. - Reasoning, tool calls, observations, and final answers retain trajectory order and shape. - Cancellation finalizes durable partial state before propagating `CancelledError` to the host. - Hermes' internal interrupt path returns a partial result rather than being confused with task cancellation. ## Live acceptance verification The default test suite is hermetic. Before a release, run the opt-in acceptance paths with an authenticated provider to exercise the real async chain rather than only mocked transports: ```bash HERMES_LIVE_TESTS=1 HERMES_LIVE_PROVIDER=copilot \ uv run pytest -q \ tests/e2e/test_live_provider_tool_path.py \ tests/e2e/test_live_provider_stream_path.py \ tests/e2e/test_live_provider_extensions_path.py \ tests/e2e/test_live_provider_state_path.py \ tests/e2e/test_live_provider_timeout_path.py \ tests/e2e/test_live_provider_concurrency_path.py \ tests/e2e/test_live_provider_subagent_path.py \ tests/e2e/test_live_provider_compression_path.py \ tests/e2e/test_live_single_runner_path.py ``` These tests cover provider-to-tool observations and trajectory ordering, a real stdio MCP server and skill loading, persistent memory and cross-instance session resume, timeout cleanup and next-turn recovery. They also verify native streaming, live context compression and continuation, the retained single-task runner, overlapping requests across agent instances, per-agent turn serialization, and background subagent reinjection and cleanup. Every path fails on event-loop blocking or leaked tasks. Set `HERMES_LIVE_MODEL` to override the provider's test default; OpenRouter runs also require `OPENROUTER_API_KEY`. A reasoning-capable provider is a separate release gate because the default Copilot acceptance model does not expose reasoning. Point this test at an already-running provider and model; for example, an LM Studio model loaded as `async-hermes-reasoning` on its default port: ```bash HERMES_LIVE_REASONING_TESTS=1 \ HERMES_LIVE_REASONING_PROVIDER=lmstudio \ HERMES_LIVE_REASONING_MODEL=async-hermes-reasoning \ uv run pytest -q \ tests/e2e/test_live_reasoning_trajectory_path.py \ tests/e2e/test_live_batch_runner_path.py ``` Those paths require reasoning on both model turns and verify the saved `reasoning β†’ tool call β†’ observation β†’ reasoning β†’ final answer` trajectory, BatchRunner checkpoint/resume and merged JSONL output, and event-loop and task cleanup. The BatchRunner gate intentionally has no non-reasoning default: upstream data-generation behavior discards samples with zero reasoning. ## Persistence and ownership `SessionDB` uses one lazily opened `aiosqlite` connection and async locks for connection and write serialization. A `SessionDB` attached to an agent, including one passed through `session_db=`, is closed by `AIAgent.close()`; passing it transfers lifecycle ownership to that agent. Host callbacks and configuration remain host-owned. For details, continue with [Agent Loop Internals](./agent-loop.md), [Upstream Differences](./upstream-differences.md), [Session Storage](./session-storage.md), and [Programmatic Integration](./programmatic-integration.md). ## Scope boundary The original Hermes Agent product also includes CLI/TUI, desktop, dashboard, messaging, ACP, and cron surfaces. They are intentionally absent here. Restore them from upstream only when their complete behavior and native-async I/O path can be carried together; do not add placeholders to the core. --- # Programmatic Integration # Programmatic Integration Async Hermes Agent is consumed as a Python library. It does not bundle an ACP server, JSON-RPC gateway, FastAPI application, or OpenAI-compatible HTTP server. An application can add any of those boundaries while keeping ownership of authentication, request validation, rate limits, and deployment. ## Smallest integration ```python from run_agent import AIAgent async def answer(message: str) -> str: async with AIAgent(...) as agent: return await agent.chat(message) ``` For full messages and metadata, await `run_conversation()` instead: ```python result = await agent.run_conversation(message) text = result["final_response"] history = result["messages"] ``` Do not call these methods from `asyncio.to_thread()`, and do not add a `run_until_complete()` wrapper inside an already-running event loop. ## Long-lived host lifecycle Create a long-lived agent for one ordered conversation and close it with the host-owned conversation lifecycle: ```python from contextlib import AsyncExitStack from run_agent import AIAgent stack = AsyncExitStack() agent = await stack.enter_async_context( AIAgent(...) ) try: result = await agent.run_conversation("Hello") finally: await stack.aclose() ``` `close()` is idempotent, so explicit fallback cleanup is safe. ## FastAPI example FastAPI is not a dependency of this package. The smallest safe example creates an isolated agent per independent request; a production host normally replaces this with a conversation-ID keyed agent/session store: ```python from fastapi import FastAPI from run_agent import AIAgent app = FastAPI() @app.post("/chat") async def chat(message: str): async with AIAgent(...) as agent: return await agent.run_conversation(message) ``` This example deliberately does not resume conversation state. A production endpoint must add its own request schema, identity-to-conversation mapping, agent/session lifecycle, authorization, timeouts, quotas, and error policy. ## Choosing the agent concurrency model A single `AIAgent` represents one mutable conversation. Its turn lock serializes concurrent calls, which is correct for ordered conversation state. Do not use one global agent when requests represent unrelated users. Common host designs are: - one agent per active conversation, cached by an application-owned session identifier; - a short-lived agent per independent job; - a bounded collection of independent agents for batch-style work. Different agents can overlap provider, database, MCP, and tool I/O. Bound the number of active agents according to provider limits and the external resources each tool can consume. ## Session identity Pass the established `session_id` constructor argument when a host needs to resume a durable conversation. Use one `SessionDB` policy consistently and avoid mapping multiple unrelated users to the same session identifier. See [Session Storage](./session-storage.md) and [Sessions](/user-guide/sessions). ## Streaming callbacks `run_conversation()` and `chat()` accept the retained `stream_callback` argument. The callback is invoked with visible text deltas while the coroutine continues toward a complete result. Keep callback work short and non-blocking; if it must perform I/O, enqueue the delta to host-owned async infrastructure instead of blocking the callback. ## Cancellation and timeouts Host cancellation propagates through provider and tool awaits. The agent first finishes its short partial-state finalizer, then re-raises `CancelledError`. Use ordinary asyncio timeout mechanisms around a turn and retain normal `finally: await agent.close()` cleanup. Do not suppress cancellation unless the host deliberately converts it into an application-level result. ## Interactive tools The retained `clarify` and approval paths require host callbacks when they need human input. A headless service should register appropriate callbacks or exclude interactive capabilities from its enabled toolsets. The library does not read from a hidden terminal as a service fallback. ## Training-data workloads For many independent prompts, use `BatchRunner` instead of constructing an unbounded set of tasks. It supplies worker limits, checkpoints, JSONL-safe writes, resume, statistics, and merged trajectories. It remains a data generation harness, not a model-training implementation. See [Python Library](/guides/python-library), [Batch Processing](/user-guide/features/batch-processing), and [Trajectory Format](./trajectory-format.md). --- # Session Storage # Session Storage `SessionDB` in `hermes_state.py` stores conversations in SQLite. Its public I/O methods are coroutines backed by `aiosqlite`, so callers do not need to add their own thread wrapper. `aiosqlite` itself serializes SQLite calls on a connection worker thread; this is an awaitable facade, not a zero-thread native SQLite transport. The default database is: ```text $HERMES_HOME/state.db ``` `HERMES_HOME` defaults to `~/.hermes`. Pass an explicit path when an embedding application should own storage placement. ## Direct use ```python from hermes_state import SessionDB db = SessionDB("/srv/my-agent/state.db") async def inspect_session(): try: await db.create_session( "conversation-1", source="api", model="example-model", ) await db.append_message("conversation-1", "user", "Hello") await db.append_message("conversation-1", "assistant", "Hi") session = await db.get_session("conversation-1") messages = await db.get_messages("conversation-1") return session, messages finally: await db.close() ``` Construction records the path only. The connection and schema are initialized lazily on the first awaited operation. ## Using storage with `AIAgent` `AIAgent` receives optional storage through the existing `session_db=` constructor argument. Without an injected `SessionDB`, an ordinary turn does not persist a transcript. A recall tool that requires session storage may instead open the default `$HERMES_HOME/state.db` lazily; from that point the agent owns the handle. With a store attached at construction, a turn creates or enriches the session row before its first provider request and incrementally persists messages during the tool loop. An injected database is borrowed by the agent: `await agent.close()` ends the agent's session work but does not close the attached `SessionDB`. The host that created the store owns its lifecycle and should close it once during application shutdown. In a service, create one store per worker lifespan and share that store among the worker's agents; do not let an individual agent close the shared store. ## Stored data The schema preserves the upstream session model, including: - session identity, source, provider/model metadata, timestamps, and working directory information; - message roles, content, tool calls, tool-call identifiers, and tool names; - reasoning and provider-specific replay metadata; - token and auxiliary-model usage accounting; - compression lineage, titles, archive state, and other session metadata; - FTS indexes used by session and message search. The system prompt is de-duplicated by hash. This supports stable prompt reuse without storing an identical large prompt on every session row. ## Core operations All operations below are awaited: | Operation | Representative methods | | --- | --- | | Create/resume | `create_session()`, `ensure_session()`, `reopen_session()` | | Append/replace | `append_message()`, `append_messages_batch()`, `replace_messages()` | | Read | `get_session()`, `get_messages()`, `get_messages_as_conversation()` | | Search | `search_messages()`, `search_sessions()`, `search_sessions_by_id()` | | Compression | `try_acquire_compression_lock()`, `archive_and_compact()`, `release_compression_lock()` | | Metadata | `update_session_meta()`, `update_session_model()`, `update_token_counts()` | | Lifecycle | `end_session()`, `delete_session()`, `close()` | Consult the method signatures in `hermes_state.py` for optional filters and return fields; that file is the canonical API reference. ## Write serialization and WAL One `SessionDB` instance lazily owns an `asyncio.Lock` for connection setup and another for writes. SQLite WAL is enabled when the filesystem supports it, with the existing journal fallback retained for filesystems where WAL is not safe or available. SQLite's busy handling and bounded retry policy remain in the database layer. Separate `SessionDB` instances can operate concurrently, subject to SQLite's normal file-level locking. A single instance still serializes mutations so transcript order remains deterministic. ## FTS search `search_messages()` uses the retained FTS5 routing and falls back according to the database's available extensions and query shape. Search and index repair remain awaited operations. Optional CJK indexing depends on the `cjk_unicode61` loadable SQLite extension being built and available for the current platform; when it is absent, the retained search fallback remains in use. ```python async def find_messages(db): return await db.search_messages( "deployment failure", role_filter=["user", "assistant"], limit=10, ) ``` `session_search` exposes this storage to the model when its toolset is enabled. ## PostgreSQL settings and read-only connections The optional PostgreSQL backend keeps the `SessionDB(db_path, read_only=False)` constructor shape. Pass an explicit `postgresql+asyncpg://` DSN; do not rely on an implicit `DATABASE_URL` lookup inside the library: ```python from hermes_state_postgres import SessionDB db = SessionDB("postgresql+asyncpg://user:password@db.example/hermes") ``` Pool and asyncpg options belong under the active profile's `config.yaml` and use the driver names directly: ```yaml database: postgres: pool_size: 5 max_overflow: 10 pool_timeout: 30 pool_recycle: -1 pool_pre_ping: true pool_use_lifo: false connect_args: timeout: 60 command_timeout: null statement_cache_size: 100 max_cached_statement_lifetime: 300 max_cacheable_statement_size: 15360 server_settings: application_name: async-hermes-agent statement_timeout: "60000" lock_timeout: "5000" idle_in_transaction_session_timeout: "600000" ``` The profile selected when the store is constructed owns these settings. The async connection is still initialized at the first awaited operation, and the validated options remain fixed for that store's lifetime. Changing the config requires a newly created store. TLS and endpoint selection remain DSN concerns. On a writable store, first initialization creates or additively reconciles the retained tables, foreign keys, and query indexes under a PostgreSQL advisory transaction lock, then records the retained schema version. It does not run Alembic or perform destructive rewrites. A read-only store only validates the existing version and never creates or migrates schema. When a future upstream release changes the SQLite schema, the corresponding PostgreSQL column/data migration must be ported and tested before that release is advertised for existing PostgreSQL databases. For a read replica or a search/diagnostic connection, use: ```python readonly_db = SessionDB(read_replica_url, read_only=True) ``` This refuses SessionDB writes and forces PostgreSQL transactions into read-only mode. It does not select a replica automatically, and it requires an already initialized schema. A separate PostgreSQL read-only role or replica endpoint provides an additional operational permission boundary. When sharing one store in a service worker, estimate the possible connection count as `workers * (pool_size + max_overflow)`. ## Crash and cancellation behavior The turn prologue persists the user message before the first provider request. Tool-loop progress is persisted incrementally, and finalization closes or repairs incomplete protocol tails before its final write. On host task cancellation, the agent shields the short finalization needed to leave durable state consistent, then propagates cancellation. JSONL trajectories are separate from `state.db`; see [Trajectory Format](./trajectory-format.md). ## Shutdown Always await `close()`. It is safe to call once in a `finally` block: ```python async def list_recent_sessions(): db = SessionDB() try: return await db.search_sessions(limit=20) finally: await db.close() ``` Reusing a closed `SessionDB` raises an error rather than silently opening a new connection. --- # Trajectory Format # Trajectory Format Async Hermes Agent can convert a completed turn into a text-oriented, ShareGPT-style conversation for downstream dataset work. It preserves the sequence needed to study interleaved reasoning and tool use: 1. system tool instructions; 2. human request; 3. assistant reasoning and tool call; 4. tool observation; 5. subsequent reasoning/tool rounds; 6. final assistant answer. The library generates and serializes trajectories. It does not train, fine- tune, score, or curate a model. ## Per-turn files Construct an agent with `save_trajectories=True`: ```python async with AIAgent( ..., save_trajectories=True, ) as agent: await agent.run_conversation("Research the issue") ``` Completed turns append one record to `trajectory_samples.jsonl` in the current working directory. Incomplete or failed turns use `failed_trajectories.jsonl`. ```json { "conversations": [], "timestamp": "2026-08-08T12:00:00", "model": "example/model", "completed": true } ``` Each append is awaited and shielded long enough to finish one JSONL record if the owning task is cancelled. ## Conversation entries `conversations` is an ordered list of objects with `from` and `value` fields. | `from` | Meaning | | --- | --- | | `system` | Function-calling instructions and the tool schemas active for the sample | | `human` | User input, including later genuine user turns if present | | `gpt` | Assistant reasoning, tool calls, narration, or final answer | | `tool` | One or more observations corresponding to the prior assistant tool calls | An abbreviated example: ```json { "conversations": [ {"from": "system", "value": "........."}, {"from": "human", "value": "Inspect the repository"}, { "from": "gpt", "value": "\nI need the tree.\n\n\n{\"name\":\"terminal\",\"arguments\":{\"command\":\"git status --short\"}}\n" }, { "from": "tool", "value": "\n{\"tool_call_id\":\"call_1\",\"name\":\"terminal\",\"content\":\"\"}\n" }, {"from": "gpt", "value": "\n\n\nThe tree is clean."} ] } ``` ## Normalization rules ### Reasoning Native reasoning is placed inside `...`. Existing `` blocks are normalized to the same tag. Every `gpt` entry receives a `` block, which may be empty, so consumers see a stable shape. ### Tool calls Each model call is serialized as JSON inside `` tags with `name` and `arguments`. Arguments are objects rather than JSON-encoded strings. ### Tool observations Consecutive observations for one assistant turn are collected into one `tool` entry, each inside its own `` tag. The payload carries `tool_call_id`, `name`, and `content`. ### Media Trajectories are text-oriented. Image-bearing tool messages use their text summary rather than embedding large base64 payloads. ### Ephemeral context `BatchRunner` disables project context files and persistent memory for each sample and does not save its optional ephemeral system prompt into the trajectory. This prevents machine-local context from silently contaminating a dataset. ## Batch records `BatchRunner` writes `data//batch_.jsonl` shards. Each successful record includes: ```json { "prompt_index": 0, "conversations": [], "metadata": { "batch_num": 0, "timestamp": "2026-08-08T12:00:00", "model": "example/model" }, "completed": true, "partial": false, "api_calls": 3, "toolsets_used": ["file", "terminal"], "tool_stats": {}, "tool_error_counts": {} } ``` At the end of a run, valid shards are merged into `data//trajectories.jsonl`. The same directory contains `checkpoint.json` and `statistics.json`. Resume scans saved prompt content and does not rely only on a process-local counter. Samples with no reasoning across all assistant turns are discarded by the batch path. Invalid JSON rows and entries containing unknown tool names are filtered during merge and reported in the run output. ## Reading JSONL ```python import json from pathlib import Path records = [ json.loads(line) for line in Path("data/my-run/trajectories.jsonl").read_text( encoding="utf-8" ).splitlines() if line.strip() ] ``` Before training, define and test your own acceptance policy for completion, tool success, reasoning quality, safety, duplication, and data provenance. Generation success is not evidence that a sample is suitable for training. See [Batch Processing](/user-guide/features/batch-processing) for runner usage. --- # Upstream Differences # Upstream differences Async Hermes Agent is based on upstream Hermes Agent `v2026.8.13` (Python package version `0.20.1`). The table below records the deliberate differences in the retained library surface. It is a migration guide, not a claim that the removed upstream applications are still shipped. | Area | Upstream `v2026.8.13` | Async Hermes Agent `0.20.1.2` | Integration impact | | --- | --- | --- | --- | | Public entry points | Retained names and module paths | The same retained names, arguments, defaults, and return shapes; I/O-bearing calls are coroutines | Existing library callers normally add `await` at the call site | | Agent construction | Synchronous upstream lifecycle | `AIAgent.__init__()` is state-only; provider, session, MCP, and plugin setup starts at an awaited boundary | Use `async with AIAgent(...)` or `await agent.close()` | | Conversation lifecycle | Synchronous turn execution | `await agent.run_conversation(...)` and `await agent.chat(...)`; turns on one agent remain serialized | Keep one agent per ordered conversation; separate agents can overlap | | I/O model | Synchronous provider, MCP, subprocess, file, and SQLite boundaries | Native coroutine transports and awaited public I/O; regular files use `aiofiles`, SQLite uses `aiosqlite` | The host event loop is not blocked by the public I/O paths; zero-thread file/SQLite I/O is not promised | | Cancellation and cleanup | Synchronous cleanup semantics | Partial state is persisted before external `CancelledError` is re-raised; owned clients, processes, and tasks are closed deterministically | Host cancellation can safely propagate through a request or job | | Concurrency | Upstream application scheduling | Same-agent turn lock, bounded batch workers, profile-scoped caches and clients | Unrelated conversations and batch items can run concurrently without sharing mutable state | | Sessions and memory | Upstream persistence behavior | Async `SessionDB`, FTS search, memory, checkpoint, export/import, and cold-process resume | `await` the existing session methods; use a stable `HERMES_HOME`/session policy | | PostgreSQL SessionDB | Upstream ships its SQLite session store | Additive `hermes_state_postgres.SessionDB` uses SQLAlchemy Core + asyncpg; `hermes_state.SessionDB` remains unchanged | Install `postgres`, inject one worker-owned store explicitly, and close it from the host lifespan; PostgreSQL ranking can differ from SQLite BM25 | | Trajectories | Upstream reasoning/tool/observation format | Same ordering and retained JSON shape, with async persistence and compression | Existing trajectory consumers can read the same retained fields | | Training-data runner | Synchronous runner boundaries | `MiniSWERunner` and `BatchRunner` keep their upstream names while becoming coroutines; checkpoint, resume, shards, merged JSONL, and statistics remain | `await runner.run_task(...)` or `await runner.run(...)` | | Profile isolation | Process-oriented environment and cache assumptions | Task-local secrets plus canonical `HERMES_HOME` state isolate concurrent profiles and symlink aliases | A/B profiles can run in one process without borrowing each other's credentials or files | | Provider policy | Synchronous adapters, including SDK-specific bootstrap behavior | Native async adapters are used where available; unsupported or unsafe synchronous paths fail explicitly rather than moving to a hidden worker thread | Install the relevant extra and follow provider-specific limitations | | FastAPI/service boundary | Upstream product applications may own service surfaces | No FastAPI server, CLI/TUI, messaging bridge, scheduler, dashboard, or desktop application is bundled | The host application owns HTTP lifecycle, auth, routing, quotas, and shutdown | | MCP and skills | Upstream product-managed discovery | Retained stdio, Streamable HTTP, and SSE MCP clients plus filesystem/external skill discovery with async lifecycle cleanup | Configure them from the host's Hermes home and close the agent at shutdown | | Optional providers and tools | Upstream distribution layout | Provider-specific extras remain opt-in (`anthropic`, `vertex`, `azure-identity`, `bedrock`, memory, web, media, and execution backends) | Install only the extras used by the selected configuration | | Python and package version | Upstream baseline `0.20.1` | Python `>=3.11,<3.14`; package `0.20.1.2` (`async_revision=2`) | The first three version segments track upstream; the fourth tracks this async distribution | ## Intentional public-surface exception The retained upstream callables preserve their public names and argument shapes. `TrajectoryCompressor.close()` is the one explicit lifecycle addition: the async port owns an async model client and needs a public cleanup boundary for it. It is not an `aclose()` alias or a synchronous compatibility wrapper. ## What is not changed The conversion does not redesign the model-tool schema, rename upstream modules, add `arun_*` aliases, or silently run synchronous provider code in a thread. Pure CPU transformations remain synchronous. Provider output, message-role alternation, tool-call ordering, trajectory ordering, checkpoint semantics, and retained return dictionaries are preserved and covered by behavior-level parity tests. For the supported feature set and installation commands, see [Installation](../getting-started/installation.md). For host lifecycle examples, see [Programmatic Integration](./programmatic-integration.md). --- # Environment Variables # Environment Variables Async Hermes Agent uses environment variables primarily for credentials and secret-adjacent paths. Behavioral settings belong in `$HERMES_HOME/config.yaml` or explicit `AIAgent` constructor arguments. The library also loads secrets from `$HERMES_HOME/.env`. Do not commit that file. ## Runtime location | Variable | Purpose | Default | | --- | --- | --- | | `HERMES_HOME` | Root for `config.yaml`, `.env`, `state.db`, memory, skills, MCP tokens, logs, and caches | `~/.hermes` | For tests and isolated services, set `HERMES_HOME` to a dedicated directory before constructing an agent. Do not repurpose the operating system's `HOME` variable as Hermes state. ## Common model-provider credentials Pass `api_key=` explicitly when that is clearer for your host. The retained provider registry also recognizes these common environment variables: | Provider | Variables | | --- | --- | | OpenRouter | `OPENROUTER_API_KEY` | | OpenAI-compatible OpenAI endpoint | `OPENAI_API_KEY` | | Anthropic | `ANTHROPIC_API_KEY`, `ANTHROPIC_TOKEN`, or `CLAUDE_CODE_OAUTH_TOKEN` according to the selected auth path | | Google AI Studio | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | | Nous | `NOUS_API_KEY` | | Fireworks | `FIREWORKS_API_KEY` | | DeepSeek | `DEEPSEEK_API_KEY` | | DeepInfra | `DEEPINFRA_API_KEY` | | NVIDIA | `NVIDIA_API_KEY` | | xAI | `XAI_API_KEY` | | Hugging Face | `HF_TOKEN` | | Azure Foundry | `AZURE_FOUNDRY_API_KEY`, with `AZURE_FOUNDRY_BASE_URL` | | Gemini on Vertex | Service-account/ADC configuration rather than a static provider key | | AWS Bedrock | Standard AWS SDK credential chain | Additional retained provider plugins declare their accepted variables in `plugins/model-providers//__init__.py`. That declaration is the canonical source when adding or auditing a provider. ## Tool and capability credentials Only configure credentials for capabilities you enable. Common examples are: | Capability | Variables | | --- | --- | | Web providers | `EXA_API_KEY`, `PARALLEL_API_KEY`, `FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, or the selected provider's key | | Image/video via FAL | `FAL_KEY` | | Image generation via Krea | `KREA_API_KEY` | | OpenAI image generation | `OPENAI_API_KEY` | | OpenRouter media | `OPENROUTER_API_KEY` | | Mem0 Platform | `MEM0_API_KEY` | | ByteRover | `BRV_API_KEY` | Capability plugin manifests under `plugins/` declare required variables. A missing optional credential should disable or fail the selected capability; it should not be replaced by an unrelated provider key. ## MCP secrets MCP configuration can interpolate process or `.env` secrets: ```yaml mcp_servers: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"] env: GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_TOKEN}" ``` `${env:GITHUB_TOKEN}` is also accepted. Prefer references over committing a literal token to `config.yaml`. See [MCP Configuration](./mcp-config-reference.md). ## Base URLs and other behavior Some provider plugins retain a provider-specific `*_BASE_URL` for established compatibility. For new host code, prefer the explicit `base_url=` argument or the documented `config.yaml` provider section. Timeouts, tool selection, reasoning policy, browser behavior, and concurrency are not secrets and should not be introduced as new environment-variable-only configuration. ## Secret handling - Keep `.env` permissions restricted and out of version control. - Never place secrets in prompts, trajectories, or checked-in datasets. - Do not forward the whole host environment to tools or MCP subprocesses. - Use an isolated `HERMES_HOME` per tenant or security boundary. - Rotate a credential if it appears in logs or an exported trajectory. See [Configuration](/user-guide/configuration) and [Providers](/integrations/providers) for the non-secret side of setup. --- # FAQ & Troubleshooting # FAQ & Troubleshooting ## Is this the full Hermes Agent product? No. It is a native-async library distribution derived from upstream Hermes Agent `v2026.8.13`. It keeps the agent loop and retained provider/tool/MCP/ skill/memory/session/trajectory surfaces. CLI/TUI, desktop/dashboard, messaging, cron, and a bundled web service are intentionally outside scope. ## Is it API-compatible with upstream? Public names and file locations are preserved where practical, but I/O-bearing methods are coroutines. Existing library code normally changes from `agent.chat(...)` to `await agent.chat(...)`, and from `agent.close()` to `await agent.close()`. This is a divergent async distribution, not a drop-in replacement for upstream product applications. ## Why is `AIAgent()` not awaited? Construction performs state-only initialization. Provider, database, plugin, and MCP work begins at `__aenter__()` or the first awaited turn. ## Do I need `arun_conversation()` or `aclose()`? No. The existing names are the async API: ```python result = await agent.run_conversation("Hello") answer = await agent.chat("Hello") await agent.close() ``` ## Can I call it from FastAPI? Yes. Create and close agents in FastAPI's lifespan and await them from route handlers. FastAPI is not bundled, and the host must supply authentication, session mapping, request limits, and error handling. See [Programmatic Integration](/developer-guide/programmatic-integration). ## Why are requests on one agent serialized? One `AIAgent` is one mutable conversation. Its turn lock protects transcript, prompt-cache, and persistence ordering. Use different agent instances for independent conversations. ## Does async automatically make every request faster? No. It allows unrelated I/O-bound work to make progress while another request waits. Provider latency, model generation, CPU-heavy work, external quotas, and tool limits still determine performance. ## Which providers work? The retained registry includes Anthropic, Gemini, Vertex, Bedrock, Codex and cloud-identity routes, many OpenAI-compatible profiles, and custom/local endpoints. Most network transports are native async; Bedrock and some identity SDK paths retain the blocking bootstrap limitations documented in [Providers](/integrations/providers). Some routes require an optional dependency extra. See [Providers](/integrations/providers) and the provider's plugin manifest. ## A provider says the key is missing Check, in order: 1. the explicit `api_key=` value passed by the host; 2. the provider-specific variable in the process environment; 3. `$HERMES_HOME/.env`; 4. that `provider`, `base_url`, and model ID refer to the same provider. Do not send `OPENAI_API_KEY` to an unrelated custom endpoint. See [Environment Variables](./environment-variables.md). ## I received a rate-limit error Rate limits belong to the selected provider/account. The library retains provider retry and credential-pool behavior, but it does not invent a local quota. Reduce concurrency, wait for the provider window, or use an account/model with suitable limits. ## MCP tools do not appear Verify that: - the entry is under `mcp_servers` in the active `HERMES_HOME/config.yaml`; - `enabled` is not false; - the stdio command exists in the subprocess `PATH`, or the URL is reachable; - include/exclude filters admit the tool; - the agent reached an awaited initialization boundary; - the agent remains open. See [MCP Configuration](./mcp-config-reference.md). ## Why does an interactive tool fail in a service? `clarify` and approval paths need host callbacks. Register a callback or omit interactive capabilities from a headless agent. The library intentionally does not fall back to an invisible terminal prompt. ## Where are sessions stored? By default, `$HERMES_HOME/state.db`, normally `~/.hermes/state.db`. For tests or multi-tenant services, use a separate `HERMES_HOME` or injected `SessionDB` policy per isolation boundary. See [Session Storage](/developer-guide/session-storage). ## Why was a batch sample discarded? The batch path discards samples with no reasoning across all assistant turns. During merge it also filters invalid JSON and records containing unknown tool names. A retained record still needs consumer-defined quality and safety review before training. ## Does this package train a model? No. It generates trajectories and batch metadata. Fine-tuning, RL, dataset curation, evaluation, and checkpoint management for a model trainer are external responsibilities. ## The event loop appears blocked Reproduce with the runtime blocking tests and identify the exact active path. Do not hide a synchronous SDK behind `asyncio.to_thread()`. Convert the I/O boundary to a native async client or fail explicitly when that capability is selected. ## What should I include in a bug report? Include the package version, Python version, platform, provider and API mode, minimal awaited example, exception traceback, and whether the issue reproduces with a temporary `HERMES_HOME`. Remove tokens, prompts, memory, and user data. --- # MCP Configuration Reference # MCP Configuration Reference Define MCP servers under `mcp_servers` in `$HERMES_HOME/config.yaml`. Discovery, tool calls, reconnection, and shutdown run as tasks on the agent's event loop. ## Stdio server ```yaml mcp_servers: filesystem: command: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] env: {} timeout: 300 connect_timeout: 60 ``` `command` selects stdio transport. `args` is optional. The subprocess receives a filtered environment plus the explicit `env` map; it does not inherit every host secret automatically. ## Streamable HTTP server ```yaml mcp_servers: remote: url: https://example.test/mcp headers: Authorization: "Bearer ${REMOTE_MCP_TOKEN}" timeout: 180 connect_timeout: 30 ``` An entry with `url` uses Streamable HTTP by default. Remote URL validation is applied before connection. ## SSE server ```yaml mcp_servers: legacy_sse: url: https://example.test/sse transport: sse timeout: 180 ``` Use `transport: sse` only for a server implementing the older MCP SSE transport. ## Common fields | Field | Meaning | | --- | --- | | `enabled` | Enable the server; defaults to `true` | | `command` | Stdio executable | | `args` | Stdio argument list | | `env` | Explicit stdio environment additions; values may reference secrets | | `url` | Remote MCP endpoint | | `transport` | `http` by default for URLs, or `sse` | | `headers` | Remote request headers | | `timeout` | Per-tool-call timeout in seconds; default `300` | | `connect_timeout` | Initial connection timeout; default `60` | | `lazy` | Register from a valid schema cache and connect on first call when possible | | `supports_parallel_tool_calls` | Opt this server's tools into parallel-safe scheduling; default `false` | | `keepalive_interval` | Liveness-ping interval; default `180`, minimum `5` seconds | | `idle_timeout_seconds` | Recycle an idle stdio server; `0` disables | | `max_lifetime_seconds` | Recycle an aged stdio server; `0` disables | | `skip_preflight` | Skip the remote content-type probe for a known valid endpoint | Lifecycle limits may also be nested below `lifecycle`. ## Tool filtering ```yaml mcp_servers: github: command: npx args: ["-y", "@modelcontextprotocol/server-github"] tools: include: ["get_*", "search_repositories"] exclude: [] resources: false prompts: true ``` - `tools.include` is a whitelist and takes precedence. - `tools.exclude` is a blacklist used only when no include list is present. - Exact names and case-sensitive `fnmatch` patterns are accepted. - `resources` and `prompts` control generated utility tools, subject to the capabilities actually advertised by the server. ## Registered names An MCP tool is exposed as: ```text mcp____ ``` For example, server `github` tool `search-repositories` becomes `mcp__github__search_repositories`. The server also contributes the dynamic toolset `mcp-github`; its raw server name is accepted as an alias by toolset resolution. Name collisions caused by sanitization fail closed rather than selecting an arbitrary handler. ## Parallel calls MCP tools are sequential by default. Set `supports_parallel_tool_calls: true` only when the server and every relevant operation are safe to overlap. Results still enter the model transcript in the original tool-call order. ## Sampling Servers that request MCP sampling can be configured under `sampling`: ```yaml sampling: enabled: true model: example/model max_tokens_cap: 4096 timeout: 30 max_rpm: 10 allowed_models: [] max_tool_rounds: 5 ``` Sampling uses host-owned model configuration and remains bounded by these limits. Disable it for servers that should never invoke an LLM. ## Lifecycle The first awaited agent boundary discovers configured servers. MCP ownership is reference-counted across active agents, and `await agent.close()` releases the agent's ownership. Always close agents so stdio subprocesses and remote sessions terminate cleanly. See [MCP](/user-guide/features/mcp) and [Use MCP with Hermes](/guides/use-mcp-with-hermes). --- # Tools Reference # Tools Reference Tool schemas are registered from their implementation modules and filtered by toolset, configuration, and each tool's availability check. The schema returned by `await model_tools.get_tool_definitions(...)` is the canonical runtime contract. ## Core retained tools | Tool | Required input | Purpose | | --- | --- | --- | | `terminal` | `command` | Run a command with the retained local terminal backend | | `process` | `action` | Inspect, poll, write to, or stop background processes | | `read_file` | `path` | Read text or supported file content | | `write_file` | `path`, `content` | Write a file subject to safety checks | | `patch` | `mode` | Apply structured file patches | | `search_files` | `pattern` | Search file names or contents | | `web_search` | `query` | Search through the configured web provider | | `web_extract` | `urls` | Extract content from one or more URLs | | `x_search` | `query` | Search public X content through xAI; opt-in | | `vision_analyze` | `image_url`, `question` | Analyze an image | | `image_generate` | `prompt` | Generate an image through the selected provider plugin | | `execute_code` | `code` | Run bounded Python orchestration over enabled Hermes tools | | `skills_list` | none | Discover available skills | | `skill_view` | `name` | Load one skill document | | `skill_manage` | `action`, `name` | Create, edit, patch, or remove a skill | | `memory` | `target` | Read or modify persistent memory/user-profile content | | `session_search` | none | Search persisted sessions and messages | | `todo` | none | Maintain turn planning state | | `clarify` | `question` | Ask the host user for clarification through a callback | | `delegate_task` | none | Launch one or more child-agent tasks; each task requires a goal | | `text_to_speech` | `text` | Synthesize an audio file through the configured TTS provider | | `computer_use` | `action` | Drive a cua-driver desktop session, subject to approval | ## Media and service tools | Tool | Required input | Availability | | --- | --- | --- | | `video_analyze` | `video_url`, `question` | Opt-in `video` toolset | | `video_generate` | `prompt` | Selected video-generation provider | | `xai_video_edit` | `prompt`, `video_url` | xAI video-generation provider | | `xai_video_extend` | `prompt`, `video_url` | xAI video-generation provider | | `bfl_flux3_text_to_video` | `prompt` | BFL/Nous managed gateway | | `bfl_flux3_image_to_video` | `prompt`, `input_image` | BFL/Nous managed gateway | | `bfl_flux3_keyframes_to_video` | `prompt`, `input_images`, `keyframe_indices` | BFL/Nous managed gateway | | `bfl_flux3_video_continuation` | `prompt`, `input_video` | BFL/Nous managed gateway | | `bfl_flux3_get_result` | `id` | Existing BFL job | | `bfl_flux3_prompting_guide` | none | BFL toolset enabled | | `ha_list_entities` | none | `HASS_TOKEN` configured | | `ha_get_state` | `entity_id` | `HASS_TOKEN` configured | | `ha_list_services` | none | `HASS_TOKEN` configured | | `ha_call_service` | `domain`, `service` | `HASS_TOKEN` configured | `text_to_speech` returns synthesized media; speaker playback belongs to the removed CLI/gateway UI and is not a separate library API. `computer_use` requires the external `cua-driver` runtime even though its compatibility extra does not add another Python package. ## Browser tools When a browser backend is configured, the `browser` toolset can expose: - `browser_navigate` - `browser_snapshot` - `browser_click` - `browser_type` - `browser_scroll` - `browser_back` - `browser_press` - `browser_get_images` - `browser_vision` - `browser_console` - `browser_cdp` - `browser_dialog` Browser schemas vary by operation and provider capability. Read the live schema instead of hard-coding optional parameters in a host. ## MCP tools Each discovered MCP tool is registered dynamically as: ```text mcp____ ``` Its parameter schema comes from the MCP server and is normalized for provider compatibility. MCP resource and prompt utility tools are added only when the server advertises those capabilities and configuration allows them. See [MCP Configuration](./mcp-config-reference.md). ## Deferred tool search When the configured tool schema would be large, the model-facing list may use the bridge tools `tool_search`, `tool_describe`, and `tool_call`. They do not create new capabilities: they defer schemas for tools already available to the agent. Calls are validated against the underlying schema before dispatch. ## Availability checks Registration does not guarantee that a tool is callable. `check_fn` gates provider credentials, browser readiness, callbacks, and other prerequisites. Unavailable tools are omitted from the model-facing list. Hosts should enable only the toolsets needed by a conversation. A smaller list reduces prompt footprint and limits accidental capability exposure. ## Async execution Tool handlers on the retained active path are awaited directly. Parallel-safe calls may overlap, but result messages retain model-call order. Interactive, stateful, or safety-sensitive calls remain sequential. A synchronous-only tool transport fails explicitly. File-backed tools still inherit the documented `aiofiles` executor limitation; they are event-loop nonblocking but must not be described as zero-thread or OS-native regular-file I/O. See [Tools and Toolsets](/user-guide/features/tools) and [Toolsets Reference](./toolsets-reference.md). --- # Toolsets Reference # Toolsets Reference Toolsets group related tool names. Pass a list through the existing `enabled_toolsets` and `disabled_toolsets` constructor arguments; runtime availability checks still apply after resolution. ```python agent = AIAgent( ..., enabled_toolsets=["file", "terminal", "skills"], ) ``` ## Static toolsets | Toolset | Contents | | --- | --- | | `web` | `web_search`, `web_extract` | | `search` | `web_search` | | `x_search` | `x_search` | | `vision` | `vision_analyze` | | `video` | `video_analyze` | | `image_gen` | `image_generate` | | `video_gen` | `video_generate`, `xai_video_edit`, `xai_video_extend` | | `bfl` | Six FLUX 3 submit, poll, and prompting-guide tools | | `computer_use` | `computer_use` | | `terminal` | `terminal`, `process` | | `file` | `read_file`, `write_file`, `patch`, `search_files` | | `skills` | `skills_list`, `skill_view`, `skill_manage` | | `browser` | Browser operations plus `web_search` | | `tts` | `text_to_speech` | | `homeassistant` | Four Home Assistant entity/state/service tools | | `todo` | `todo` | | `memory` | `memory` | | `session_search` | `session_search` | | `clarify` | `clarify` | | `delegation` | `delegate_task` | | `context_engine` | Tools supplied by the selected context-engine plugin | ## Composite toolsets | Toolset | Behavior | | --- | --- | | `debugging` | Terminal/process plus the `web` and `file` toolsets | | `safe` | Web, vision, and image generation without terminal access | | `coding` | Coding-oriented files, terminal, web, skills, browser, planning, memory, clarification, and delegation | | `hermes-cli` | Historical upstream name for the full retained library tool list; no CLI application is included | The `safe` name means β€œwithout terminal access”; it is not a complete security sandbox. It includes external web calls and image generation. ## Dynamic toolsets Built-in discovery also registers `code_execution` with `execute_code`. It is not authored in the static `TOOLSETS` mapping, but it resolves through the same registry-backed public helpers after tool discovery. ### MCP A configured server named `github` contributes `mcp-github`. The raw server name is also recognized as an alias. Contents follow live discovery and the server's include/exclude filters. ### Plugins Retained plugins can register tools into an existing or plugin-defined toolset. Toolset resolution merges registry contributions without mutating the static `TOOLSETS` table. ## Resolution rules - Included toolsets are expanded recursively according to their definitions. - Disabled tools/toolsets are filtered after expansion. - Registry aliases resolve MCP and plugin names. - Availability checks can remove tools whose backend or credential is absent. - `all` or `*` expands registered toolsets, but it does not bypass an availability or safety check. Use `get_toolset()`, `resolve_toolset()`, and `get_all_toolsets()` from `toolsets.py` when a host needs to inspect the resolved configuration. See [Tools Reference](./tools-reference.md). ---