mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-10-20 05:15:31 +00:00
Merge remote-tracking branch 'origin/main' into feat/iron-proxy
# Conflicts: # hermes_cli/main.py # tools/environments/docker.py
This commit is contained in:
commit
42139c3cf0
2618 changed files with 339158 additions and 65016 deletions
|
|
@ -9,7 +9,7 @@ This guide covers adding a new messaging platform to the Hermes gateway. A platf
|
|||
:::tip
|
||||
There are two ways to add a platform:
|
||||
- **Plugin** (recommended for community/third-party): Drop a plugin directory into `~/.hermes/plugins/` — zero core code changes needed. See [Plugin Path](#plugin-path-recommended) below.
|
||||
- **Built-in**: Modify 20+ files across code, config, and docs. Use the [Built-in Checklist](#step-by-step-checklist) below.
|
||||
- **Built-in**: Modify 20+ files across code, config, and docs. Use the [Built-in Checklist](#step-by-step-checklist-built-in-path) below.
|
||||
:::
|
||||
|
||||
## Architecture Overview
|
||||
|
|
@ -34,11 +34,11 @@ The plugin system lets you add a platform adapter without modifying any core Her
|
|||
|
||||
```
|
||||
~/.hermes/plugins/my-platform/
|
||||
PLUGIN.yaml # Plugin metadata
|
||||
plugin.yaml # Plugin metadata
|
||||
adapter.py # Adapter class + register() entry point
|
||||
```
|
||||
|
||||
### PLUGIN.yaml
|
||||
### plugin.yaml
|
||||
|
||||
Plugin metadata. The `requires_env` and `optional_env` blocks auto-populate `hermes config` UI entries (see [Surfacing Env Vars](#surfacing-env-vars-in-hermes-config) below).
|
||||
|
||||
|
|
|
|||
|
|
@ -321,12 +321,12 @@ At minimum, touch the tests that guard provider wiring.
|
|||
|
||||
Common places:
|
||||
|
||||
- `tests/test_runtime_provider_resolution.py`
|
||||
- `tests/test_cli_provider_resolution.py`
|
||||
- `tests/test_cli_model_command.py`
|
||||
- `tests/test_setup_model_selection.py`
|
||||
- `tests/test_provider_parity.py`
|
||||
- `tests/test_run_agent.py`
|
||||
- `tests/hermes_cli/test_runtime_provider_resolution.py`
|
||||
- `tests/cli/test_cli_provider_resolution.py`
|
||||
- `tests/hermes_cli/test_model_switch_custom_providers.py` (and adjacent `tests/hermes_cli/test_model_switch_*.py`)
|
||||
- `tests/hermes_cli/test_setup_model_provider.py`
|
||||
- `tests/run_agent/test_provider_parity.py`
|
||||
- `tests/run_agent/test_run_agent.py`
|
||||
- `tests/test_<provider>_adapter.py` for a native provider
|
||||
|
||||
For docs-only examples, the exact file set may differ. The point is to cover:
|
||||
|
|
@ -342,7 +342,7 @@ Run tests with xdist disabled:
|
|||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python -m pytest tests/test_runtime_provider_resolution.py tests/test_cli_provider_resolution.py tests/test_cli_model_command.py tests/test_setup_model_selection.py -n0 -q
|
||||
python -m pytest tests/hermes_cli/test_runtime_provider_resolution.py tests/cli/test_cli_provider_resolution.py tests/hermes_cli/test_setup_model_provider.py tests/run_agent/test_provider_parity.py -n0 -q
|
||||
```
|
||||
|
||||
For deeper changes, run the full suite before pushing:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: "Detailed walkthrough of AIAgent execution, API modes, tools, callb
|
|||
|
||||
# Agent Loop Internals
|
||||
|
||||
The core orchestration engine is `run_agent.py`'s `AIAgent` class — a large file (15k+ lines) that handles everything from prompt assembly to tool dispatch to provider failover.
|
||||
The core orchestration engine is `run_agent.py`'s `AIAgent` class — a large file that handles everything from prompt assembly to tool dispatch to provider failover.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ This page is the top-level map of Hermes Agent internals. Use it to orient yours
|
|||
▼ ▼
|
||||
┌───────────────────┐ ┌──────────────────────┐
|
||||
│ Session Storage │ │ Tool Backends │
|
||||
│ (SQLite + FTS5) │ │ Terminal (7 backends) │
|
||||
│ (SQLite + FTS5) │ │ Terminal (6 backends) │
|
||||
│ hermes_state.py │ │ Browser (5 backends) │
|
||||
│ gateway/session.py│ │ Web (4 backends) │
|
||||
└───────────────────┘ │ MCP (dynamic) │
|
||||
|
|
@ -130,7 +130,7 @@ hermes-agent/
|
|||
├── skills/ # Bundled skills (always available)
|
||||
├── optional-skills/ # Official optional skills (install explicitly)
|
||||
├── website/ # Docusaurus documentation site
|
||||
└── tests/ # Pytest suite (~3,000+ tests)
|
||||
└── tests/ # Pytest suite (~25,000 tests across ~1,250 files)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
|
@ -197,7 +197,7 @@ The synchronous orchestration engine (`AIAgent` in `run_agent.py`). Handles prov
|
|||
|
||||
Prompt construction and maintenance across the conversation lifecycle:
|
||||
|
||||
- **`prompt_builder.py`** — Assembles the system prompt from: personality (SOUL.md), memory (MEMORY.md, USER.md), skills, context files (AGENTS.md, .hermes.md), tool-use guidance, and model-specific instructions
|
||||
- **`system_prompt.py` + `prompt_builder.py`** — assembles the ordered system-prompt tiers (`stable` → `context` → `volatile`): identity/tool guidance/skills, context files, then memory/profile/timestamp blocks
|
||||
- **`prompt_caching.py`** — Applies Anthropic cache breakpoints for prefix caching
|
||||
- **`context_compressor.py`** — Summarizes middle conversation turns when context exceeds thresholds
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ A shared runtime resolver used by CLI, gateway, cron, ACP, and auxiliary calls.
|
|||
|
||||
### Tool System
|
||||
|
||||
Central tool registry (`tools/registry.py`) with 70+ registered tools across ~28 toolsets. Each tool file self-registers at import time. The registry handles schema collection, dispatch, availability checking, and error wrapping. Terminal tools support 7 backends (local, Docker, SSH, Daytona, Modal, Singularity, Vercel Sandbox).
|
||||
Central tool registry (`tools/registry.py`) with 70+ registered tools across ~28 toolsets. Each tool file self-registers at import time. The registry handles schema collection, dispatch, availability checking, and error wrapping. Terminal tools support 6 backends (local, Docker, SSH, Daytona, Modal, Singularity).
|
||||
|
||||
→ [Tools Runtime](./tools-runtime.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +1,49 @@
|
|||
# Browser CDP Supervisor — Design
|
||||
---
|
||||
sidebar_position: 18
|
||||
title: "Browser CDP Supervisor"
|
||||
description: "How Hermes detects and responds to native JS dialogs and interacts with cross-origin iframes via a persistent CDP connection."
|
||||
---
|
||||
|
||||
**Status:** Shipped (PR 14540)
|
||||
**Last updated:** 2026-04-23
|
||||
**Author:** @teknium1
|
||||
# Browser CDP Supervisor
|
||||
|
||||
## Problem
|
||||
The CDP supervisor closes two long-standing gaps in Hermes' browser tooling:
|
||||
|
||||
Native JS dialogs (`alert`/`confirm`/`prompt`/`beforeunload`) and iframes are
|
||||
the two biggest gaps in our browser tooling:
|
||||
1. **Native JS dialogs** (`alert`/`confirm`/`prompt`/`beforeunload`) block the
|
||||
page's JS thread. Without supervision, the agent has no way to know a
|
||||
dialog is open — subsequent tool calls hang or throw opaque errors.
|
||||
2. **Cross-origin iframes (OOPIFs)** are invisible to top-level
|
||||
`Runtime.evaluate`. The agent can see iframe nodes in the DOM snapshot but
|
||||
can't click, type, or eval inside them without a CDP session attached to
|
||||
the child target.
|
||||
|
||||
1. **Dialogs block the JS thread.** Any operation on the page stalls until the
|
||||
dialog is handled. Before this work, the agent had no way to know a dialog
|
||||
was open — subsequent tool calls would hang or throw opaque errors.
|
||||
2. **Iframes are invisible.** The agent could see iframe nodes in the DOM
|
||||
snapshot but could not click, type, or eval inside them — especially
|
||||
cross-origin (OOPIF) iframes that live in separate Chromium processes.
|
||||
The supervisor solves both by holding a persistent WebSocket to the backend's
|
||||
CDP endpoint per browser task, surfacing pending dialogs and frame structure
|
||||
into `browser_snapshot`, and exposing a `browser_dialog` tool for explicit
|
||||
responses.
|
||||
|
||||
[PR #12550](https://github.com/NousResearch/hermes-agent/pull/12550) proposed a
|
||||
stateless `browser_dialog` wrapper. That doesn't solve detection — it's a
|
||||
cleaner CDP call for when the agent already knows (via symptoms) that a dialog
|
||||
is open. Closed as superseded.
|
||||
|
||||
## Backend capability matrix (verified live 2026-04-23)
|
||||
|
||||
Using throwaway probe scripts against a data-URL page that fires alerts in the
|
||||
main frame and in a same-origin srcdoc iframe, plus a cross-origin
|
||||
`https://example.com` iframe:
|
||||
## Backend support
|
||||
|
||||
| Backend | Dialog detect | Dialog respond | Frame tree | OOPIF `Runtime.evaluate` via `browser_cdp(frame_id=...)` |
|
||||
|---|---|---|---|---|
|
||||
| Local Chrome (`--remote-debugging-port`) / `/browser connect` | ✓ | ✓ full workflow | ✓ | ✓ |
|
||||
| Browserbase | ✓ (via bridge) | ✓ full workflow (via bridge) | ✓ | ✓ (`document.title = "Example Domain"` verified on real cross-origin iframe) |
|
||||
| Browserbase | ✓ (via bridge) | ✓ full workflow (via bridge) | ✓ | ✓ |
|
||||
| Camofox | ✗ no CDP (REST-only) | ✗ | partial via DOM snapshot | ✗ |
|
||||
|
||||
**How Browserbase respond works.** Browserbase's CDP proxy uses Playwright
|
||||
internally and auto-dismisses native dialogs within ~10ms, so
|
||||
`Page.handleJavaScriptDialog` can't keep up. To work around this, the
|
||||
supervisor injects a bridge script via
|
||||
**Browserbase quirk.** Browserbase's CDP proxy uses Playwright internally and
|
||||
auto-dismisses native dialogs within ~10ms, so `Page.handleJavaScriptDialog`
|
||||
can't keep up. The supervisor injects a bridge script via
|
||||
`Page.addScriptToEvaluateOnNewDocument` that overrides
|
||||
`window.alert`/`confirm`/`prompt` with a synchronous XHR to a magic host
|
||||
(`hermes-dialog-bridge.invalid`). `Fetch.enable` intercepts those XHRs
|
||||
before they touch the network — the dialog becomes a `Fetch.requestPaused`
|
||||
event the supervisor captures, and `respond_to_dialog` fulfills via
|
||||
(`hermes-dialog-bridge.invalid`). `Fetch.enable` intercepts those XHRs before
|
||||
they touch the network — the dialog becomes a `Fetch.requestPaused` event the
|
||||
supervisor captures, and `respond_to_dialog` fulfills via
|
||||
`Fetch.fulfillRequest` with a JSON body the injected script decodes.
|
||||
|
||||
Net result: from the page's perspective, `prompt()` still returns the
|
||||
agent-supplied string. From the agent's perspective, it's the same
|
||||
`browser_dialog(action=...)` API either way. Tested end-to-end against
|
||||
real Browserbase sessions — 4/4 (alert/prompt/confirm-accept/confirm-dismiss)
|
||||
pass including value round-tripping back into page JS.
|
||||
From the page's perspective, `prompt()` still returns the agent-supplied
|
||||
string. From the agent's perspective, it's the same `browser_dialog(action=...)`
|
||||
API either way.
|
||||
|
||||
Camofox stays unsupported for this PR; follow-up upstream issue planned at
|
||||
`jo-inc/camofox-browser` requesting a dialog polling endpoint.
|
||||
Camofox is unsupported — no CDP surface, REST-only.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -63,9 +55,10 @@ Holds a persistent WebSocket to the backend's CDP endpoint. Maintains:
|
|||
- **Dialog queue** — `List[PendingDialog]` with `{id, type, message, default_prompt, session_id, opened_at}`
|
||||
- **Frame tree** — `Dict[frame_id, FrameInfo]` with parent relationships, URL, origin, whether cross-origin child session
|
||||
- **Session map** — `Dict[session_id, SessionInfo]` so interaction tools can route to the right attached session for OOPIF operations
|
||||
- **Recent console errors** — ring buffer of the last 50 (for PR 2 diagnostics)
|
||||
- **Recent console errors** — ring buffer of the last 50 for diagnostics
|
||||
|
||||
Subscribes on attach:
|
||||
|
||||
- `Page.enable` — `javascriptDialogOpening`, `frameAttached`, `frameNavigated`, `frameDetached`
|
||||
- `Runtime.enable` — `executionContextCreated`, `consoleAPICalled`, `exceptionThrown`
|
||||
- `Target.setAutoAttach {autoAttach: true, flatten: true}` — surfaces child OOPIF targets; supervisor enables `Page`+`Runtime` on each
|
||||
|
|
@ -76,11 +69,13 @@ frozen snapshot without awaiting.
|
|||
### Lifecycle
|
||||
|
||||
- **Start:** `SupervisorRegistry.get_or_start(task_id, cdp_url)` — called by
|
||||
`browser_navigate`, Browserbase session create, `/browser connect`. Idempotent.
|
||||
`browser_navigate`, Browserbase session create, `/browser connect`.
|
||||
Idempotent.
|
||||
- **Stop:** session teardown or `/browser disconnect`. Cancels the asyncio
|
||||
task, closes the WebSocket, discards state.
|
||||
- **Rebind:** if the CDP URL changes (user reconnects to a new Chrome), stop
|
||||
the old supervisor and start fresh — never reuse state across endpoints.
|
||||
- **Rebind:** if the CDP URL changes (user reconnects to a new Chrome), the
|
||||
old supervisor is stopped and a fresh one started — state is never reused
|
||||
across endpoints.
|
||||
|
||||
### Dialog policy
|
||||
|
||||
|
|
@ -92,14 +87,14 @@ Configurable via `config.yaml` under `browser.dialog_policy`:
|
|||
forever.
|
||||
- `auto_dismiss` — record and dismiss immediately; agent sees it after the
|
||||
fact via `browser_state` inside `browser_snapshot`.
|
||||
- `auto_accept` — record and accept (useful for `beforeunload` where the user
|
||||
wants to navigate away cleanly).
|
||||
- `auto_accept` — record and accept (useful for `beforeunload` where the
|
||||
workflow wants to navigate away cleanly).
|
||||
|
||||
Policy is per-task; no per-dialog overrides in v1.
|
||||
Policy is per-task; no per-dialog overrides.
|
||||
|
||||
## Agent surface (PR 1)
|
||||
## Agent surface
|
||||
|
||||
### One new tool
|
||||
### `browser_dialog` tool
|
||||
|
||||
```
|
||||
browser_dialog(action, prompt_text=None, dialog_id=None)
|
||||
|
|
@ -107,9 +102,9 @@ browser_dialog(action, prompt_text=None, dialog_id=None)
|
|||
|
||||
- `action="accept"` / `"dismiss"` → responds to the specified or sole pending dialog (required)
|
||||
- `prompt_text=...` → text to supply to a `prompt()` dialog
|
||||
- `dialog_id=...` → disambiguate when multiple dialogs queued (rare)
|
||||
- `dialog_id=...` → disambiguate when multiple dialogs are queued (rare)
|
||||
|
||||
Tool is response-only. Agent reads pending dialogs from `browser_snapshot`
|
||||
Tool is response-only. The agent reads pending dialogs from `browser_snapshot`
|
||||
output before calling.
|
||||
|
||||
### `browser_snapshot` extension
|
||||
|
|
@ -137,72 +132,52 @@ is attached:
|
|||
}
|
||||
```
|
||||
|
||||
- **`pending_dialogs`**: dialogs currently blocking the page's JS thread.
|
||||
- **`pending_dialogs`** — dialogs currently blocking the page's JS thread.
|
||||
The agent must call `browser_dialog(action=...)` to respond. Empty on
|
||||
Browserbase because their CDP proxy auto-dismisses within ~10ms.
|
||||
|
||||
- **`recent_dialogs`**: ring buffer of up to 20 recently-closed dialogs with
|
||||
a `closed_by` tag — `"agent"` (we responded), `"auto_policy"` (local
|
||||
- **`recent_dialogs`** — ring buffer of up to 20 recently-closed dialogs with
|
||||
a `closed_by` tag: `"agent"` (we responded), `"auto_policy"` (local
|
||||
auto_dismiss/auto_accept), `"watchdog"` (must_respond timeout hit), or
|
||||
`"remote"` (browser/backend closed it on us, e.g. Browserbase). This is
|
||||
how agents on Browserbase still get visibility into what happened.
|
||||
|
||||
- **`frame_tree`**: frame structure including cross-origin (OOPIF) children.
|
||||
- **`frame_tree`** — frame structure including cross-origin (OOPIF) children.
|
||||
Capped at 30 entries + OOPIF depth 2 to bound snapshot size on ad-heavy
|
||||
pages. `truncated: true` surfaces when limits were hit; agents needing
|
||||
the full tree can use `browser_cdp` with `Page.getFrameTree`.
|
||||
|
||||
No new tool schema surface for any of these — the agent reads the snapshot
|
||||
it already requests.
|
||||
No new tool schema surface for any of these — the agent reads the snapshot it
|
||||
already requests.
|
||||
|
||||
### Availability gating
|
||||
|
||||
Both surfaces gate on `_browser_cdp_check` (supervisor can only run when a CDP
|
||||
endpoint is reachable). On Camofox / no-backend sessions, the dialog tool is
|
||||
hidden and snapshot omits the new fields — no schema bloat.
|
||||
hidden and the snapshot omits the new fields — no schema bloat.
|
||||
|
||||
## Cross-origin iframe interaction
|
||||
|
||||
Extending the dialog-detect work, `browser_cdp(frame_id=...)` routes CDP
|
||||
calls (notably `Runtime.evaluate`) through the supervisor's already-connected
|
||||
WebSocket using the OOPIF's child `sessionId`. Agents pick frame_ids out of
|
||||
`browser_cdp(frame_id=...)` routes CDP calls (notably `Runtime.evaluate`)
|
||||
through the supervisor's already-connected WebSocket using the OOPIF's child
|
||||
`sessionId`. Agents pick frame_ids out of
|
||||
`browser_snapshot.frame_tree.children[]` where `is_oopif=true` and pass them
|
||||
to `browser_cdp`. For same-origin iframes (no dedicated CDP session), the
|
||||
agent uses `contentWindow`/`contentDocument` from a top-level
|
||||
`Runtime.evaluate` instead — supervisor surfaces an error pointing at that
|
||||
`Runtime.evaluate` instead — the supervisor surfaces an error pointing at that
|
||||
fallback when `frame_id` belongs to a non-OOPIF.
|
||||
|
||||
On Browserbase, this is the ONLY reliable path for iframe interaction —
|
||||
On Browserbase, this is the only reliable path for iframe interaction —
|
||||
stateless CDP connections (opened per `browser_cdp` call) hit signed-URL
|
||||
expiry, while the supervisor's long-lived connection keeps a valid session.
|
||||
|
||||
## Camofox (follow-up)
|
||||
|
||||
Issue planned against `jo-inc/camofox-browser` adding:
|
||||
- Playwright `page.on('dialog', handler)` per session
|
||||
- `GET /tabs/:tabId/dialogs` polling endpoint
|
||||
- `POST /tabs/:tabId/dialogs/:id` to accept/dismiss
|
||||
- Frame-tree introspection endpoint
|
||||
|
||||
## Files touched (PR 1)
|
||||
|
||||
### New
|
||||
## File layout
|
||||
|
||||
- `tools/browser_supervisor.py` — `CDPSupervisor`, `SupervisorRegistry`, `PendingDialog`, `FrameInfo`
|
||||
- `tools/browser_dialog_tool.py` — `browser_dialog` tool handler
|
||||
- `tests/tools/test_browser_supervisor.py` — mock CDP WebSocket server + lifecycle/state tests
|
||||
- `website/docs/developer-guide/browser-supervisor.md` — this file
|
||||
|
||||
### Modified
|
||||
|
||||
- `toolsets.py` — register `browser_dialog` in `browser`, `hermes-acp`, `hermes-api-server`, core toolsets (gated on CDP reachability)
|
||||
- `tools/browser_tool.py`
|
||||
- `browser_navigate` start-hook: if CDP URL resolvable, `SupervisorRegistry.get_or_start(task_id, cdp_url)`
|
||||
- `browser_snapshot` (at ~line 1536): merge supervisor state into return payload
|
||||
- `/browser connect` handler: restart supervisor with new endpoint
|
||||
- Session teardown hooks in `_cleanup_browser_session`
|
||||
- `hermes_cli/config.py` — add `browser.dialog_policy` and `browser.dialog_timeout_s` to `DEFAULT_CONFIG`
|
||||
- Docs: `website/docs/user-guide/features/browser.md`, `website/docs/reference/tools-reference.md`, `website/docs/reference/toolsets-reference.md`
|
||||
- `tools/browser_tool.py` — `browser_navigate` start-hook, `browser_snapshot` merge, `/browser connect` reattach, `_cleanup_browser_session` teardown
|
||||
- `toolsets.py` — registers `browser_dialog` in `browser`, `hermes-acp`, `hermes-api-server`, and core toolsets (gated on CDP reachability)
|
||||
- `hermes_cli/config.py` — `browser.dialog_policy` and `browser.dialog_timeout_s` defaults
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
|
@ -214,9 +189,10 @@ Issue planned against `jo-inc/camofox-browser` adding:
|
|||
|
||||
## Testing
|
||||
|
||||
Unit tests use an asyncio mock CDP server that speaks enough of the protocol
|
||||
to exercise all state transitions: attach, enable, navigate, dialog fire,
|
||||
dialog dismiss, frame attach/detach, child target attach, session teardown.
|
||||
Real-backend E2E (Browserbase + local Chromium-family browser) is manual — exercise via
|
||||
`/browser connect` to a live Chromium-family browser and run the dialog/frame
|
||||
test cases described above.
|
||||
Unit tests (`tests/tools/test_browser_supervisor.py`) use an asyncio mock CDP
|
||||
server that speaks enough of the protocol to exercise all state transitions:
|
||||
attach, enable, navigate, dialog fire, dialog dismiss, frame attach/detach,
|
||||
child target attach, session teardown. Real-backend E2E (Browserbase + local
|
||||
Chromium-family browser) is manual — exercise via `/browser connect` to a
|
||||
live Chromium-family browser and run the dialog/frame test cases described
|
||||
above.
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ compression:
|
|||
threshold: 0.50 # Fraction of context window (default: 0.50 = 50%)
|
||||
target_ratio: 0.20 # How much of threshold to keep as tail (default: 0.20)
|
||||
protect_last_n: 20 # Minimum protected tail messages (default: 20)
|
||||
codex_gpt55_autoraise: true # gpt-5.5 on Codex OAuth: raise trigger to 85% (default: true)
|
||||
|
||||
# Summarization model/provider configured under auxiliary:
|
||||
auxiliary:
|
||||
|
|
@ -101,6 +102,22 @@ auxiliary:
|
|||
| `target_ratio` | `0.20` | 0.10-0.80 | Controls tail protection token budget: `threshold_tokens × target_ratio` |
|
||||
| `protect_last_n` | `20` | ≥1 | Minimum number of recent messages always preserved |
|
||||
| `protect_first_n` | `3` | (hardcoded) | System prompt + first exchange always preserved |
|
||||
| `codex_gpt55_autoraise` | `true` | bool | Raise the trigger to 85% for gpt-5.5 on the ChatGPT Codex OAuth route (see below). Set `false` to keep the global `threshold` |
|
||||
|
||||
### Codex gpt-5.5 threshold autoraise
|
||||
|
||||
The ChatGPT Codex OAuth backend hard-caps gpt-5.5 at a **272K** context window
|
||||
(the same slug exposes 1.05M on OpenAI's direct API and OpenRouter, and 400K on
|
||||
GitHub Copilot). At the default 50% trigger, compaction would fire at ~136K —
|
||||
half the window the model can actually use. When the active route is Codex
|
||||
OAuth (`provider: openai-codex`) and the model is gpt-5.5, Hermes raises the
|
||||
trigger to **85%** (~231K) and prints a one-time notice with the opt-out
|
||||
command. Only this exact route is affected; gpt-5.5 on any other provider keeps
|
||||
your global `threshold`. To opt back down to the global value:
|
||||
|
||||
```bash
|
||||
hermes config set compression.codex_gpt55_autoraise false
|
||||
```
|
||||
|
||||
### Computed Values (for a 200K context model at defaults)
|
||||
|
||||
|
|
@ -111,6 +128,17 @@ tail_token_budget = 100,000 × 0.20 = 20,000
|
|||
max_summary_tokens = min(200,000 × 0.05, 12,000) = 10,000
|
||||
```
|
||||
|
||||
:::note Threshold is derived from the MAIN model's context window
|
||||
`threshold_tokens` is always `threshold × context_length`, where `context_length`
|
||||
is the **main agent model's** context window — never the auxiliary/summary
|
||||
model's. On a 262,144-token model at the default `0.50`, the threshold is
|
||||
`262,144 × 0.50 = 131,072`. That number being close to a common "128K context"
|
||||
is a coincidence of the percentage, not a sign that the auxiliary model's window
|
||||
is the trigger. The auxiliary model's context window is a separate concern — see
|
||||
the "Summary model context length" warning below for how it affects whether a
|
||||
summary can be produced, not when compression fires.
|
||||
:::
|
||||
|
||||
|
||||
## Compression Algorithm
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ We value contributions in this order:
|
|||
|
||||
| Requirement | Notes |
|
||||
|-------------|-------|
|
||||
| **Git** | With `--recurse-submodules` support, and the `git-lfs` extension installed |
|
||||
| **Git** | With the `git-lfs` extension installed |
|
||||
| **Python 3.11+** | uv will install it if missing |
|
||||
| **uv** | Fast Python package manager ([install](https://docs.astral.sh/uv/)) |
|
||||
| **Node.js 20+** | Optional — needed for browser tools and WhatsApp bridge (matches root `package.json` engines) |
|
||||
|
|
@ -41,7 +41,7 @@ We value contributions in this order:
|
|||
### Clone and Install
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/NousResearch/hermes-agent.git
|
||||
git clone https://github.com/NousResearch/hermes-agent.git
|
||||
cd hermes-agent
|
||||
|
||||
# Create venv with Python 3.11
|
||||
|
|
@ -94,7 +94,7 @@ pytest tests/ -v
|
|||
|
||||
## Cross-Platform Compatibility
|
||||
|
||||
Hermes officially supports **Linux, macOS, WSL2, and native Windows (early beta — via PowerShell install)**. Native Windows uses Git Bash (from [Git for Windows](https://git-scm.com/download/win)) for shell commands. A few features require POSIX kernel primitives and are gated: the dashboard's embedded PTY terminal pane (`/chat` tab) is WSL2-only. The native-Windows path is new and moves fast — if you're doing Windows-heavy dev, expect to hit and fix rough edges.
|
||||
Hermes officially supports **Linux, macOS, WSL2, and native Windows (via PowerShell install)**. Native Windows uses Git Bash (from [Git for Windows](https://git-scm.com/download/win)) for shell commands. A few features require POSIX kernel primitives and are gated: the dashboard's embedded PTY terminal pane (`/chat` tab) is WSL2-only. If you're doing Windows-heavy dev, run the Windows-footgun lint (`scripts/check-windows-footguns.py`) before pushing.
|
||||
|
||||
When contributing code, keep these rules in mind:
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ Bundled skills (in `skills/`) ship with every Hermes install. They should be **b
|
|||
- Document handling, web research, common dev workflows, system administration
|
||||
- Used regularly by a wide range of people
|
||||
|
||||
If your skill is official and useful but not universally needed (e.g., a paid service integration, a heavyweight dependency), put it in **`optional-skills/`** — it ships with the repo, is discoverable via `hermes skills browse` (labeled "official"), and installs with builtin trust.
|
||||
If your skill is official and useful but not universally needed (e.g., a paid service integration, a heavyweight dependency), put it in **`optional-skills/`** — it ships with the repo, is discoverable via `hermes skills browse` (labeled "official"), and installs with built-in trust.
|
||||
|
||||
If your skill is specialized, community-contributed, or niche, it's better suited for a **Skills Hub** — upload it to a registry and share it via `hermes skills install`.
|
||||
|
||||
|
|
@ -363,7 +363,7 @@ All hub-installed skills go through a security scanner that checks for:
|
|||
|
||||
Trust levels:
|
||||
- `builtin` — ships with Hermes (always trusted)
|
||||
- `official` — from `optional-skills/` in the repo (builtin trust, no third-party warning)
|
||||
- `official` — from `optional-skills/` in the repo (built-in trust, no third-party warning)
|
||||
- `trusted` — from openai/skills, anthropics/skills, huggingface/skills
|
||||
- `community` — non-dangerous findings can be overridden with `--force`; `dangerous` verdicts remain blocked
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class MyMemoryProvider(MemoryProvider):
|
|||
| `is_available()` | Agent init, before activation | **Yes** — no network calls |
|
||||
| `initialize(session_id, **kwargs)` | Agent startup | **Yes** |
|
||||
| `get_tool_schemas()` | After init, for tool injection | **Yes** |
|
||||
| `handle_tool_call(name, args)` | When agent uses your tools | **Yes** (if you have tools) |
|
||||
| `handle_tool_call(tool_name, args, **kwargs)` | When agent uses your tools | **Yes** (if you have tools) |
|
||||
|
||||
### Config
|
||||
|
||||
|
|
@ -75,9 +75,9 @@ class MyMemoryProvider(MemoryProvider):
|
|||
| Method | When Called | Use Case |
|
||||
|--------|-----------|----------|
|
||||
| `system_prompt_block()` | System prompt assembly | Static provider info |
|
||||
| `prefetch(query)` | Before each API call | Return recalled context |
|
||||
| `prefetch(query, *, session_id="")` | Before each API call | Return recalled context |
|
||||
| `queue_prefetch(query)` | After each turn | Pre-warm for next turn |
|
||||
| `sync_turn(user, assistant)` | After each completed turn | Persist conversation |
|
||||
| `sync_turn(user, assistant, *, session_id="")` | After each completed turn | Persist conversation |
|
||||
| `on_session_end(messages)` | Conversation ends | Final extraction/flush |
|
||||
| `on_pre_compress(messages)` | Before context compression | Save insights before discard |
|
||||
| `on_memory_write(action, target, content)` | Built-in memory writes | Mirror to your backend |
|
||||
|
|
@ -154,10 +154,10 @@ hooks:
|
|||
**`sync_turn()` MUST be non-blocking.** If your backend has latency (API calls, LLM processing), run the work in a daemon thread:
|
||||
|
||||
```python
|
||||
def sync_turn(self, user_content, assistant_content):
|
||||
def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None):
|
||||
def _sync():
|
||||
try:
|
||||
self._api.ingest(user_content, assistant_content)
|
||||
self._api.ingest(user_content, assistant_content, session_id=session_id, messages=messages)
|
||||
except Exception as e:
|
||||
logger.warning("Sync failed: %s", e)
|
||||
|
||||
|
|
@ -167,6 +167,16 @@ def sync_turn(self, user_content, assistant_content):
|
|||
self._sync_thread.start()
|
||||
```
|
||||
|
||||
`messages` is optional OpenAI-style conversation context as of the completed
|
||||
turn. When present, it includes user/assistant messages, assistant tool calls,
|
||||
and tool result messages. Providers that do not need raw turn context can omit
|
||||
the `messages` parameter; Hermes will continue calling them with the legacy
|
||||
signature.
|
||||
|
||||
Cloud providers should document what parts of `messages` are sent off-device.
|
||||
Tool calls and tool results may contain file paths, command output, or other
|
||||
workspace data.
|
||||
|
||||
## Profile Isolation
|
||||
|
||||
All storage paths **must** use the `hermes_home` kwarg from `initialize()`, not hardcoded `~/.hermes`:
|
||||
|
|
@ -182,7 +192,7 @@ data_dir = Path("~/.hermes/my-provider").expanduser()
|
|||
|
||||
## Testing
|
||||
|
||||
See `tests/agent/test_memory_plugin_e2e.py` for the complete E2E testing pattern using a real SQLite provider.
|
||||
See `tests/agent/test_memory_provider.py` and adjacent memory tests (`tests/agent/test_memory_session_switch.py`, `tests/agent/test_memory_user_id.py`, `tests/run_agent/test_memory_provider_init.py`) for end-to-end patterns.
|
||||
|
||||
```python
|
||||
from agent.memory_manager import MemoryManager
|
||||
|
|
|
|||
|
|
@ -131,8 +131,9 @@ class AcmeProfile(ProviderProfile):
|
|||
|
||||
def build_api_kwargs_extras(self, *, reasoning_config=None, **context):
|
||||
"""Returns (extra_body_additions, top_level_kwargs). Needed when some
|
||||
fields go top-level (Kimi's reasoning_effort) and some go in extra_body
|
||||
(OpenRouter's reasoning dict). Default: ({}, {})."""
|
||||
fields go top-level (Kimi's reasoning_effort, OpenRouter's verbosity for
|
||||
adaptive Anthropic models) and some go in extra_body (OpenRouter's
|
||||
reasoning dict). Default: ({}, {})."""
|
||||
return {}, {}
|
||||
|
||||
def fetch_models(self, *, api_key=None, timeout=8.0) -> list[str] | None:
|
||||
|
|
@ -250,7 +251,7 @@ The general `PluginManager` (the thing `hermes plugins` operates on) **sees** mo
|
|||
Like any Hermes plugin, model providers can ship as a pip package. Add an entry point to your `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project.entry-points."hermes.plugins"]
|
||||
[project.entry-points."hermes_agent.plugins"]
|
||||
acme-inference = "acme_hermes_plugin:register"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ hermes acp --bootstrap # print install snippet for an ACP-capable IDE
|
|||
|
||||
```
|
||||
prompt.submit prompt.background session.steer
|
||||
session.create session.list session.interrupt
|
||||
session.create session.list session.active_list
|
||||
session.activate session.close session.interrupt
|
||||
session.history session.compress session.branch
|
||||
session.title session.usage session.status
|
||||
clarify.respond sudo.respond secret.respond
|
||||
|
|
@ -52,6 +53,8 @@ delegation.status subagent.interrupt spawn_tree.save / list / load
|
|||
terminal.resize clipboard.paste image.attach
|
||||
```
|
||||
|
||||
`session.active_list`, `session.activate`, and `session.close` are the process-local live-session controls used by the TUI session switcher. Use `session.list` / `/resume` for saved transcript discovery; use the active-session methods only for sessions that are currently open in the TUI gateway process.
|
||||
|
||||
### Events streamed back
|
||||
|
||||
`message.delta`, `message.complete`, `tool.start`, `tool.progress`, `tool.complete`, `approval.request`, `clarify.request`, `sudo.request`, `secret.request`, `gateway.ready`, plus session lifecycle and error events.
|
||||
|
|
|
|||
|
|
@ -26,18 +26,18 @@ Primary files:
|
|||
|
||||
## Cached system prompt layers
|
||||
|
||||
The cached system prompt is assembled in roughly this order:
|
||||
The cached system prompt is assembled as three ordered tiers (see `agent/system_prompt.py`):
|
||||
|
||||
1. agent identity — `SOUL.md` from `HERMES_HOME` when available, otherwise falls back to `DEFAULT_AGENT_IDENTITY` in `prompt_builder.py`
|
||||
2. tool-aware behavior guidance
|
||||
3. Honcho static block (when active)
|
||||
4. optional system message
|
||||
5. frozen MEMORY snapshot
|
||||
6. frozen USER profile snapshot
|
||||
7. skills index
|
||||
8. context files (`AGENTS.md`, `.cursorrules`, `.cursor/rules/*.mdc`) — SOUL.md is **not** included here when it was already loaded as the identity in step 1
|
||||
9. timestamp / optional session ID
|
||||
10. platform hint
|
||||
1. **stable** — identity (`SOUL.md` or fallback), tool/model guidance, skills prompt, environment hints, platform hints
|
||||
2. **context** — caller-supplied `system_message` plus project context files (`.hermes.md` / `AGENTS.md` / `CLAUDE.md` / `.cursorrules`)
|
||||
3. **volatile** — built-in memory snapshot (`MEMORY.md`), user profile snapshot (`USER.md`), external memory-provider block, timestamp/session/model/provider line
|
||||
|
||||
The final system prompt is then joined as: `stable` → `context` → `volatile`.
|
||||
|
||||
This ordering matters for precedence discussions:
|
||||
- skills are part of the **stable** tier
|
||||
- memory/profile snapshots are part of the **volatile** tier
|
||||
- both are still in the cached system prompt (they are not injected as ad-hoc mid-turn overlays)
|
||||
|
||||
When `skip_context_files` is set (e.g., subagent delegation), SOUL.md is not loaded and the hardcoded `DEFAULT_AGENT_IDENTITY` is used instead.
|
||||
|
||||
|
|
@ -205,13 +205,15 @@ These are intentionally *not* persisted as part of the cached system prompt:
|
|||
- `ephemeral_system_prompt`
|
||||
- prefill messages
|
||||
- gateway-derived session context overlays
|
||||
- later-turn Honcho recall injected into the current-turn user message
|
||||
- later-turn Honcho/external recall injected into the current-turn user message
|
||||
|
||||
`pre_llm_call` plugin context also lands in this API-call-time path: it is appended to the current turn's **user message**, not written into the cached system prompt. When multiple plugins return context, Hermes concatenates those context blocks (see [Hooks → `pre_llm_call`](../user-guide/features/hooks.md#pre_llm_call)).
|
||||
|
||||
This separation keeps the stable prefix stable for caching.
|
||||
|
||||
## Memory snapshots
|
||||
|
||||
Local memory and user profile data are injected as frozen snapshots at session start. Mid-session writes update disk state but do not mutate the already-built system prompt until a new session or forced rebuild occurs.
|
||||
Local memory and user profile data are captured in the system prompt's **volatile tier**. Mid-session writes update disk state but do not mutate the already-built cached system prompt until a rebuild path runs (new session, or explicit invalidation/rebuild flow such as compression-triggered rebuild).
|
||||
|
||||
## Context files
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ That ordering matters because Hermes treats the saved model/provider choice as t
|
|||
|
||||
Current provider families include (see `plugins/model-providers/` for the complete bundled set):
|
||||
|
||||
- AI Gateway (Vercel)
|
||||
- OpenRouter
|
||||
- Nous Portal
|
||||
- OpenAI Codex
|
||||
|
|
@ -93,18 +92,13 @@ This resolver is the main reason Hermes can share auth/runtime logic between:
|
|||
- ACP editor sessions
|
||||
- auxiliary model tasks
|
||||
|
||||
## AI Gateway
|
||||
## OpenRouter and custom OpenAI-compatible base URLs
|
||||
|
||||
Set `AI_GATEWAY_API_KEY` in `~/.hermes/.env` and run with `--provider ai-gateway`. Hermes fetches available models from the gateway's `/models` endpoint, filtering to language models with tool-use support.
|
||||
|
||||
## OpenRouter, AI Gateway, and custom OpenAI-compatible base URLs
|
||||
|
||||
Hermes contains logic to avoid leaking the wrong API key to a custom endpoint when multiple provider keys exist (e.g. `OPENROUTER_API_KEY`, `AI_GATEWAY_API_KEY`, and `OPENAI_API_KEY`).
|
||||
Hermes contains logic to avoid leaking the wrong API key to a custom endpoint when multiple provider keys exist (e.g. `OPENROUTER_API_KEY` and `OPENAI_API_KEY`).
|
||||
|
||||
Each provider's API key is scoped to its own base URL:
|
||||
|
||||
- `OPENROUTER_API_KEY` is only sent to `openrouter.ai` endpoints
|
||||
- `AI_GATEWAY_API_KEY` is only sent to `ai-gateway.vercel.sh` endpoints
|
||||
- `OPENAI_API_KEY` is used for custom endpoints and as a fallback
|
||||
|
||||
Hermes also distinguishes between:
|
||||
|
|
@ -115,7 +109,7 @@ Hermes also distinguishes between:
|
|||
That distinction is especially important for:
|
||||
|
||||
- local model servers
|
||||
- non-OpenRouter/non-AI Gateway OpenAI-compatible APIs
|
||||
- non-OpenRouter OpenAI-compatible APIs
|
||||
- switching providers without re-running setup
|
||||
- config-saved custom endpoints that should keep working even when `OPENAI_BASE_URL` is not exported in the current shell
|
||||
|
||||
|
|
@ -199,7 +193,11 @@ Cron jobs **do** support fallback: `run_job()` reads `fallback_providers` (or le
|
|||
|
||||
### Test coverage
|
||||
|
||||
See `tests/test_fallback_model.py` for comprehensive tests covering all supported providers, one-shot semantics, and edge cases.
|
||||
Fallback behavior is exercised across several suites:
|
||||
|
||||
- `tests/run_agent/test_fallback_credential_isolation.py` — credential isolation between primary and fallback
|
||||
- `tests/hermes_cli/test_fallback_cmd.py` — the `/fallback` CLI command
|
||||
- `tests/gateway/test_fallback_eviction.py` — gateway eviction of failed providers
|
||||
|
||||
## Related docs
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,6 @@ The terminal system supports multiple backends:
|
|||
- singularity
|
||||
- modal
|
||||
- daytona
|
||||
- vercel_sandbox
|
||||
|
||||
It also supports:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: "How to build a web-search/extract/crawl backend plugin for Hermes
|
|||
|
||||
# Building a Web Search Provider Plugin
|
||||
|
||||
Web-search provider plugins register a backend that services `web_search`, `web_extract`, and (optionally) deep-crawl tool calls. Built-in providers — Firecrawl, SearXNG, Tavily, Exa, Parallel, Brave Search (free tier), and DDGS — all ship as plugins under `plugins/web/<name>/`. You can add a new one, or override a bundled one, by dropping a directory next to them.
|
||||
Web-search provider plugins register a backend that services `web_search`, `web_extract`, and (optionally) deep-crawl tool calls. Built-in providers — Firecrawl, SearXNG, Tavily, Exa, Parallel, Brave Search (free tier), xAI, and DDGS — all ship as plugins under `plugins/web/<name>/`. You can add a new one, or override a bundled one, by dropping a directory next to them.
|
||||
|
||||
:::tip
|
||||
Web search is one of several **backend plugins** Hermes supports. The others (with their own ABCs) are [Image Generation Provider Plugins](/developer-guide/image-gen-provider-plugin), [Video Generation Provider Plugins](/developer-guide/video-gen-provider-plugin), [Memory Provider Plugins](/developer-guide/memory-provider-plugin), [Context Engine Plugins](/developer-guide/context-engine-plugin), and [Model Provider Plugins](/developer-guide/model-provider-plugin). General tool/hook/CLI plugins live in [Build a Hermes Plugin](/guides/build-a-hermes-plugin).
|
||||
|
|
@ -43,7 +43,7 @@ plugins/web/my-backend/
|
|||
|
||||
## The WebSearchProvider ABC
|
||||
|
||||
Subclass `agent.web_search_provider.WebSearchProvider`. The only required members are `name`, `is_available()`, and whichever of `search()` / `extract()` / `crawl()` you implement.
|
||||
Subclass `agent.web_search_provider.WebSearchProvider`. The only required members are `name`, `is_available()`, and whichever of `search()` / `extract()` you implement. (Deep crawling is not a separate method — it's a mode of `extract()`.)
|
||||
|
||||
```python
|
||||
# plugins/web/my-backend/provider.py
|
||||
|
|
@ -80,9 +80,6 @@ class MyBackendWebSearchProvider(WebSearchProvider):
|
|||
def supports_extract(self) -> bool:
|
||||
return False
|
||||
|
||||
def supports_crawl(self) -> bool:
|
||||
return False
|
||||
|
||||
def search(self, query: str, limit: int = 5) -> Dict[str, Any]:
|
||||
import httpx
|
||||
|
||||
|
|
@ -157,12 +154,10 @@ Full contract in `agent/web_search_provider.py`. Methods you may override:
|
|||
| `is_available()` | ✅ | — | Cheap availability gate — env vars, optional deps |
|
||||
| `supports_search()` | — | `True` | Capability flag for `web_search` routing |
|
||||
| `supports_extract()` | — | `False` | Capability flag for `web_extract` routing |
|
||||
| `supports_crawl()` | — | `False` | Capability flag for deep-crawl modes |
|
||||
| `search(query, limit)` | conditional | raises | Required when `supports_search()` returns `True` |
|
||||
| `extract(urls, **kwargs)` | conditional | raises | Required when `supports_extract()` returns `True` |
|
||||
| `crawl(url, **kwargs)` | conditional | raises | Required when `supports_crawl()` returns `True` |
|
||||
|
||||
Providers can advertise multiple capabilities from a single class — Firecrawl, Tavily, Exa, and Parallel all implement all three of search/extract/crawl. Brave Search and DDGS are search-only; SearXNG is search-only with a documented "pair me with an extract provider" workflow.
|
||||
Providers can advertise multiple capabilities from a single class — Firecrawl, Tavily, Exa, and Parallel all implement both search and extract. Brave Search and DDGS are search-only; SearXNG is search-only with a documented "pair me with an extract provider" workflow.
|
||||
|
||||
## Response shape
|
||||
|
||||
|
|
@ -231,7 +226,7 @@ The `web_search` and `web_extract` tools live in `tools/web_tools.py`. At call t
|
|||
1. Read the relevant config key (`web.search_backend` for `web_search`, `web.extract_backend` for `web_extract`)
|
||||
2. Ask the registry for the provider with that `name`
|
||||
3. Check `is_available()` and the matching `supports_*()` flag
|
||||
4. Dispatch to `search()` / `extract()` / `crawl()`, awaiting if the method is a coroutine
|
||||
4. Dispatch to `search()` / `extract()` (deep crawl runs as a mode inside `extract()`), awaiting if the method is a coroutine
|
||||
5. JSON-serialize the response envelope and hand it back to the LLM
|
||||
|
||||
Errors surface as the tool result; the LLM decides how to explain them. If no provider is registered (or every available one fails the capability gate), the tool returns a helpful error pointing at `hermes tools`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue