agent — Core Agent Engine

Core Agent Engine

The orchestration and utility layer powering Hermes Agent — pluggable context management, memory provider abstraction, smart model routing, error classification, credential pooling, prompt building, and trajectory capture for RL training.

🎯 Why This Module Exists

The agent/ module was extracted from a monolithic 3,600-line run_agent.py that had grown to mix orchestration logic with utility functions, making testing, maintenance, and extension increasingly painful. Without this separation, every change to error handling would risk breaking context compression; every new memory backend would require modifying the core conversation loop; and unit testing any individual subsystem would require spinning up the entire agent stack. By extracting pure, self-contained utilities into agent/, the project achieves clean separation of concerns: run_agent.py focuses solely on the conversation orchestration loop, while agent/ provides the pluggable engines and services it consults. This modular architecture enables independent testing of compression, error classification, and credential management; allows third parties to swap implementations via abstract base classes (ContextEngine, MemoryProvider); and ensures the same utilities work across CLI, gateway, and cron contexts. When Anthropic releases a new caching strategy or a provider changes its error signatures, updates localize to a single file rather than scattering across the entire codebase.

💡 Real-world analogy

Think of agent/ as the engine room of a large ship. The bridge (run_agent.py) decides where to go and when to change course, but the engine room houses all the specialized machinery — the engines, fuel pumps, electrical generators, and damage-control systems — each operated by specialists. A captain doesn't rewire the generator to change heading; they issue orders through well-defined interfaces. Similarly, the orchestration loop doesn't know how context compression works internally; it simply asks the ContextEngine "should we compress?" and receives a clean answer. If the ship upgrades from diesel to electric, only the engine room changes — the bridge controls remain the same.

28
Files
~15K
Lines
8
Sub-Systems
Python
Language

Context Engine Memory Management Smart Routing Prompt Building

Architecture

flowchart LR
  subgraph ContextMgmt["Context Management"]
    CE["ContextEngine"]
    CC["ContextCompressor"]
    CR["ContextReferences"]
  end
  subgraph MemorySys["Memory System"]
    MM["MemoryManager"]
    MP["MemoryProvider"]
  end
  subgraph ModelRouting["Model and Routing"]
    SMR["SmartModelRouting"]
    CP["CredentialPool"]
    UP["UsagePricing"]
    MM2["ModelMetadata"]
  end
  subgraph ErrorSys["Error Handling"]
    EC["ErrorClassifier"]
    RU["RetryUtils"]
    RLT["RateLimitTracker"]
  end
  subgraph Core["Core Pipeline"]
    PB["PromptBuilder"]
    TC["TrajectoryCapture"]
    DI["Display"]
  end

  PB -->|"system prompt"| CE
  CE -->|"compress check"| CC
  CC -->|"summary request"| CE
  PB -->|"memory context"| MM
  MM -->|"recall/sync"| MP
  PB -->|"route decision"| SMR
  SMR -->|"model lookup"| MM2
  SMR -->|"credential"| CP
  CP -->|"cost tracking"| UP
  EC -->|"retry policy"| RU
  RU -->|"rate state"| RLT
  CE -->|"token usage"| TC
  TC -->|"formatted output"| DI
  CR -->|"ref injection"| PB

Data Flow

sequenceDiagram
    participant U as User
    participant PB as PromptBuilder
    participant MM as MemoryManager
    participant CE as ContextEngine
    participant SMR as SmartModelRouting
    participant CP as CredentialPool
    participant LLM as LLM Provider
    participant EC as ErrorClassifier
    participant RU as RetryUtils
    participant TC as TrajectoryCapture
    participant DI as Display

    U->>PB: User query
    PB->>MM: prefetch_all() recall context
    MM-->>PB: Memory context block
    PB->>CE: Check context budget
    CE->>CE: should_compress_preflight()
    CE-->>PB: Token estimate OK
    PB->>SMR: Route decision (simple vs complex)
    SMR->>CP: Select credential
    CP-->>SMR: API key + provider
    SMR->>LLM: API call with messages
    LLM-->>EC: Response or Exception
    EC->>RU: Recovery action (retry/rotate/fallback)
    RU-->>SMR: Retry with backoff
    LLM-->>TC: Successful response + usage
    TC->>DI: Format and render output
    DI-->>U: Formatted response

🚶 Step-by-Step Narrated Walkthrough

Follow a single user query through the agent pipeline as it flows through the entire agent module:

  1. User Input Received — The user types a message. SubdirectoryHintTracker checks any recent tool calls for new directory paths and lazily loads AGENTS.md / .cursorrules hints if the agent has navigated to a new directory.
  2. Memory Prefetch — MemoryManager.prefetch_all() fires across all registered providers (built-in + at most one external). Each provider retrieves relevant context from its backend. Results are merged into a fenced memory context block via build_memory_context_block().
  3. System Prompt Assembly — prompt_builder._build_system_prompt() constructs the system prompt by combining identity text, platform hints, the skills index (parsed from skill frontmatter via skill_commands.py), injected context files, and the memory block. Prompt injection detection scans AGENTS.md content for suspicious patterns.
  4. Preflight Token Check — model_metadata.estimate_messages_tokens_rough() uses a 4-chars/token heuristic with provider-specific adjustments to estimate whether the assembled messages fit within the model's context window. If the estimate exceeds the threshold, ContextEngine.should_compress_preflight() signals compression is needed.
  5. Context Compression (if triggered) — ContextCompressor.compress() prunes old tool results, protects head and tail messages, serializes middle turns, and calls the auxiliary LLM to generate a structured summary. If a previous summary exists, it uses iterative update mode to preserve accumulated facts.
  6. Smart Routing Decision — SmartModelRouting evaluates whether the user message is "simple" (under 160 chars, under 28 words, no code blocks or complex keywords). If simple, it routes to a configured cheap model to save ~80% on token cost. Otherwise, the primary model is used.
  7. Credential Selection — CredentialPool selects the next available API credential via round-robin or least-used strategy. Exhausted credentials (those with recent 429/402 errors) are skipped until their cooldown expires.
  8. Prompt Caching — For Anthropic models, apply_anthropic_cache_control() injects up to 4 cache_control breakpoints (system prompt + last 3 non-system messages) to achieve ~75% input cost reduction on cache hits.
  9. API Call & Response — The request is sent to the selected LLM provider. Response headers (x-ratelimit-*) are parsed by RateLimitTracker to update RPM/TPM bucket state. ContextEngine.update_from_response(usage) records actual token counts.
  10. Error Classification & Recovery — If the API call throws an exception, classify_api_error() runs the error through a priority-ordered pipeline: provider-specific patterns, HTTP status codes, message pattern matching, transport heuristics, and context overflow detection. The resulting ClassifiedError determines whether to retry with backoff, rotate credentials, trigger compression, fall back to another model, or abort.
  11. Trajectory Capture — On success, TrajectoryCapture records the full turn (input messages, output, tool calls, token usage, timing) for reinforcement learning training data. This creates structured datasets that can train future model versions.
  12. Display Formatting — display.py formats the assistant response for terminal output with syntax highlighting, markdown rendering, and token usage statistics. The final formatted response is presented to the user, and MemoryManager.sync_all() writes the exchange to all registered memory providers in the background.

🏗️ Design Decisions

DecisionChoice MadeAlternatives ConsideredWhy This Approach
Context compression strategy Lossy LLM summarization with iterative updates; previous summary preserved and incrementally updated on each compression (A) Retrieve-augmented generation via vector DB; (B) Sliding window dropping oldest N turns; (C) Lossless DAG construction Lossy summarization is simplest and works well for typical agent workflows. RAG requires external infrastructure; sliding window loses critical decision context from mid-conversation; DAG is complex to implement and maintain. Iterative updates prevent cascading information loss across multiple compressions in long sessions.
Memory provider abstraction Single external provider limit — built-in always active plus at most one external (Honcho, Mem0, Hindsight, etc.) (A) Multiple simultaneous providers with union schema; (B) Per-turn switchable provider Prevents tool schema bloat (20+ memory tools confuse the model); avoids ambiguous ownership of facts across providers; forces explicit configuration choice. The single-provider constraint is the mental-model winner for both users and the LLM.
Smart model routing Conservative heuristic: route to cheap model only when message is under 160 chars, under 28 words, no code blocks, no complex keywords (A) ML-based complexity classifier; (B) Always use primary model; (C) User-selected per-turn Conservative criteria ensure the cheap route only fires for clearly simple messages ("thanks", "what time is it?"). ML classifier adds training overhead and latency; always-primary wastes ~80% cost on trivial turns; user selection adds friction.
Credential pooling Multi-key round-robin with exhaustion tracking and cooldown TTL per credential (A) Single credential with retry; (B) Random selection; (C) Adaptive load balancing based on rate-limit headers Round-robin distributes load evenly; exhaustion tracking with cooldown prevents retrying dead credentials; adaptive balancing requires header parsing first (adds latency) and not all providers return consistent headers.
Error classification taxonomy Priority-ordered pipeline: provider-specific patterns, HTTP status codes, message patterns, transport heuristics, then fallback (A) Machine-learned error classifier; (B) Per-provider manual error maps; (C) Simple regex catch-all Deterministic and debuggable; no ML training overhead; centralized logic instead of scattered per-provider if/else blocks. Provider-specific patterns are folded in as high-priority matchers within the same pipeline.
Jittered backoff seed time.time_ns() ^ (counter * 0x9E3779B9) with process-level counter (A) Pure random.Random(); (B) Fixed exponential backoff; (C) Rate-limit-header-adaptive delay Pure random can produce collisions on coarse clocks; fixed backoff causes thundering herd when multiple sessions retry simultaneously. The counter-XOR-golden-ratio seed ensures diversity even when sessions start at the same instant.
Subdirectory hints injection Lazy-loaded via tool results, not injected into system prompt (A) Upfront filesystem scan at startup; (B) Batch inject all hints into system prompt Lazy loading preserves prompt caching (no system prompt edits per turn). Upfront scanning adds startup latency. Tool-result injection is transparent to the model and avoids cache invalidation.

🔬 Algorithm Spotlight

Context Compression with Iterative Summary Updates

The Problem: Long agent conversations can exceed the model's context window (e.g., 200K or 1M tokens). Naively dropping old messages loses critical decision context — file paths, design choices, error resolutions — that the agent needs to continue working effectively.

The Approach: When last_prompt_tokens exceeds a configurable threshold (default: 75% of max context), the compressor divides the conversation into three zones: a protected head (system prompt + opening exchange), a middle (summarization target), and a protected tail (recent messages by token budget, ~20K tokens). The middle is serialized with truncation (6K chars per message), then sent to an auxiliary LLM with a structured prompt requesting a handoff summary. On subsequent compressions, the previous summary is passed to the LLM with instructions to update it — moving items from "In Progress" to "Done", adding new facts, and preserving unresolved questions.

Why This Algorithm: Iterative updates prevent the cascading information loss that would occur if each compression started from scratch. A fresh summarization of the most recent middle chunk would forget facts from the previous compression. The iterative approach creates a durable, ever-growing handoff artifact. The summary budget scales with input size: max(2000, min(0.20 * content_tokens, 12000)), capped to avoid the summary itself consuming too much context.

graph TD
    A["Conversation exceeds threshold"] --> B{"Previous summary exists?"}
    B -->|"No"| C["Fresh summarization prompt"]
    B -->|"Yes"| D["Iterative update prompt"]
    C --> E["Prune old tool results"]
    D --> E
    E --> F["Protect head messages"]
    F --> G["Protect tail by token budget"]
    G --> H["Serialize middle turns"]
    H --> I["Call auxiliary LLM"]
    I --> J{"LLM call succeeded?"}
    J -->|"Yes"| K["Insert summary + tail"]
    J -->|"No"| L["Drop middle, enter 600s cooldown"]
    K --> M["Store as _previous_summary"]
    L --> M
Trace through an example

Given: A 180K-token conversation on a model with 200K context (threshold = 150K). This is the second compression; a previous summary of 3K tokens exists from an earlier compression.

Step 1: should_compress(180000) returns true since 180K ≥ 150K threshold.

Step 2: Tool pruning walks backward, replacing tool outputs older than the tail boundary with "[Old tool output cleared]". This typically recovers 10-30K tokens cheaply.

Step 3: Head protection keeps messages 0-2 (system + first exchange). Tail protection accumulates backward until ~20K tokens are covered — roughly the last 8-12 messages.

Step 4: The middle turns (~140K tokens after pruning) are serialized with 6K char truncation per message. The previous 3K-token summary is included. The iterative update prompt instructs: "Update this summary. Preserve all file paths and decisions. Move completed items to Done."

Step 5: The auxiliary LLM generates a ~6K-token updated summary (budget = min(140K * 0.20, 12K) = 12K max). The output replaces the middle: head + summary message + tail. Total context drops from 180K to ~28K tokens.

Result: The conversation continues with full recent context and a structured summary preserving decisions from all prior compressions. Without iterative updates, facts from the first compression would be completely lost.

⚠️ What Could Go Wrong

Failure ModeTriggerDetectionRecovery / Mitigation
Rate limit exhaustion All credentials in the pool hit 429 rate limits simultaneously during high-concurrency usage RateLimitTracker parses x-ratelimit-remaining headers; CredentialPool tracks last_error_code per entry jittered_backoff() computes decorrelated delays; credentials enter cooldown until TTL expires or provider reset header received; worst case, user sees a retry-after delay message
Context overflow Conversation grows beyond model context window; preflight check missed due to tool output expansion classify_api_error() matches HTTP 413 or "context", "token limit" patterns; also infers overflow when server disconnects on large sessions (>80% context used) Sets should_compress=true on the ClassifiedError; triggers ContextCompressor.compress() on next turn; tool pruning provides immediate relief
Memory backend failure External memory provider (Honcho, Mem0, Hindsight) is unreachable or returns errors during prefetch/sync Exception caught in MemoryManager.prefetch_all() per-provider try/except; logged at debug level Non-fatal: failed provider is skipped; merged results from other providers (including built-in) are used. Sync failures are also non-fatal; data may be partially lost but conversation continues
Credential pool empty All credentials exhausted (402 billing) with no remaining alternatives in the pool CredentialPool.select_next() returns None when all entries are in exhausted state Falls through to the auxiliary client fallback chain (OpenRouter, Nous Portal, Codex, Anthropic); if entire chain is exhausted, returns error to user with clear "no credentials available" message
Compression LLM failure Auxiliary LLM for summarization fails (all providers exhausted or network error) Exception from call_llm(task="compression") caught in ContextCompressor.compress() Enters 600-second cooldown; drops middle turns without summary rather than crashing; accepts slightly reduced context budget until cooldown expires and compression can be retried
Prompt injection in context files Malicious AGENTS.md or .cursorrules contains "ignore previous instructions" patterns Regex scanning in prompt_builder.py detects exfiltration and override patterns during system prompt assembly Suspicious content is flagged or stripped; injection attempt is logged; system prompt integrity is preserved

⚡ Performance Profile

Hot Paths

The most performance-sensitive code paths in this module:

Resource Usage

Key Components

ComponentFileRole
PromptBuilderprompt_builder.pyAssembles system prompt: identity, platform hints, skills index, context files with injection detection, memory blocks
ContextEnginecontext_engine.pyAbstract base class for pluggable context management; defines lifecycle (init, compress, update, status)
ContextCompressorcontext_compressor.pyDefault context engine; lossy LLM summarization with iterative updates, tool pruning, head/tail protection
MemoryManagermemory_manager.pyOrchestrates built-in + one external memory provider; handles prefetch, sync, and single-external-provider constraint
MemoryProvidermemory_provider.pyAbstract base for persistent recall backends (Honcho, Hindsight, Mem0, built-in file store)
SmartModelRoutingsmart_model_routing.pyRoutes simple queries to a cheap model; conservative heuristic based on length, word count, and complexity indicators
CredentialPoolcredential_pool.pyMulti-credential failover with round-robin rotation, exhaustion tracking, and cooldown TTL per credential
ErrorClassifiererror_classifier.pyPriority-ordered taxonomy mapping exceptions to recovery actions: retry, rotate, fallback, compress, abort
RetryUtilsretry_utils.pyDecorrelated exponential backoff with counter-based jitter to prevent thundering-herd retry spikes
RateLimitTrackerrate_limit_tracker.pyParses x-ratelimit-* headers; tracks RPM/TPM buckets, remaining capacity, and reset times
TrajectoryCapturetrajectory.pyRecords full turn data (messages, tool calls, usage, timing) as structured JSON for RL training datasets
UsagePricingusage_pricing.pyCalculates per-turn and per-session cost from token usage and model pricing tables
AuxiliaryClientauxiliary_client.pyUnified LLM resolution chain for side tasks (compression, title gen, vision); auto-fallback on 402
PromptCachingprompt_caching.pyInjects Anthropic cache_control breakpoints (system + last 3 messages) for ~75% cost reduction
Displaydisplay.pyTerminal output formatting with syntax highlighting, markdown rendering, and token usage statistics

Sub-Modules

Context Management

Pluggable context engine with compression, references, and token tracking.

  • context_engine.py — Abstract base class
  • context_compressor.py — Lossy summarization engine
  • context_references.py — External reference injection
  • manual_compression_feedback.py — User-triggered compression

Memory System

Provider-based persistent recall with 8+ backend support.

  • memory_manager.py — Multi-provider orchestration
  • memory_provider.py — Abstract provider interface
  • insights.py — Session analytics engine

Model & Routing

Smart routing, metadata, credential management, and cost tracking.

  • smart_model_routing.py — Cheap-model route detection
  • model_metadata.py — Context lengths, token estimation
  • credential_pool.py — Multi-key rotation
  • usage_pricing.py — Per-turn cost calculation
  • anthropic_adapter.py — Anthropic-specific adaptations

Skills Engine

Skill discovery, parsing, and system prompt injection.

  • skill_commands.py — Skill index and payload loading
  • skill_utils.py — Frontmatter parsing utilities
  • subdirectory_hints.py — Lazy AGENTS.md discovery

Error Handling

Classification, retry logic, and rate limit awareness.

  • error_classifier.py — Priority-ordered error taxonomy
  • retry_utils.py — Jittered exponential backoff
  • rate_limit_tracker.py — RPM/TPM header parsing

Utilities

Cross-cutting concerns: prompts, caching, display, capture, security.

  • prompt_builder.py — System prompt construction
  • prompt_caching.py — Anthropic cache breakpoints
  • auxiliary_client.py — Side-task LLM fallback chain
  • copilot_acp_client.py — Copilot ACP integration
  • trajectory.py — RL training data capture
  • title_generator.py — Async conversation titling
  • display.py — Terminal output formatting
  • redact.py — Secret masking (~15 regex patterns)

Directory Map

Source tree for agent/
agent/
├── prompt_builder.py              ← System prompt assembly with injection detection
├── context_engine.py              ← Abstract base for pluggable context management
├── context_compressor.py          ← Lossy LLM summarization with iterative updates
├── context_references.py          ← External reference injection into context
├── memory_manager.py              ← Multi-provider memory orchestration
├── memory_provider.py             ← Abstract base for recall backends
├── skill_commands.py              ← Skill index loading and payload resolution
├── skill_utils.py                 ← YAML frontmatter parsing for skills
├── smart_model_routing.py         ← Cheap-model route detection heuristic
├── model_metadata.py              ← Context lengths, token estimation (4 chars/tok)
├── credential_pool.py             ← Multi-key rotation with exhaustion tracking
├── error_classifier.py            ← Priority-ordered error taxonomy (50+ patterns)
├── retry_utils.py                 ← Jittered exponential backoff (counter XOR seed)
├── rate_limit_tracker.py          ← x-ratelimit-* header parsing, RPM/TPM buckets
├── trajectory.py                  ← RL training data capture (structured JSON)
├── usage_pricing.py               ← Per-turn and per-session cost calculation
├── display.py                     ← Terminal formatting, syntax highlighting, stats
├── anthropic_adapter.py           ← Anthropic-specific API adaptations
├── auxiliary_client.py            ← Side-task LLM fallback chain resolution
├── copilot_acp_client.py          ← GitHub Copilot ACP protocol client
├── insights.py                    ← Session analytics (tokens, costs, tool usage)
├── title_generator.py             ← Async background title generation
├── redact.py                      ← Regex-based secret masking before output
├── prompt_caching.py              ← Anthropic cache_control breakpoint injection
├── subdirectory_hints.py          ← Lazy AGENTS.md/.cursorrules discovery
├── manual_compression_feedback.py ← User-triggered manual compression support
├── __init__.py                    ← Package init
└── __pycache__/                   ← Bytecode cache (gitignored)