# 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.
## 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).
---