II.
Page JSON
Structured · livepage:docs-hermes-research-raw-agent-loop
Agent Loop Internals json
Inspect the normalized record payload exactly as the atlas UI reads it.
{
"id": "page:docs-hermes-research-raw-agent-loop",
"_kind": "Page",
"_file": "wiki/docs/hermes-research/raw/agent-loop.md",
"_cluster": "wiki",
"attributes": {
"nodeKind": "Page",
"sourcePath": "docs/hermes-research/raw/agent-loop.md",
"sourceKind": "repo-docs",
"title": "Agent Loop Internals",
"displayName": "Agent Loop Internals",
"slug": "docs/hermes-research/raw/agent-loop",
"articlePath": "wiki/docs/hermes-research/raw/agent-loop.md",
"article": "\n# Agent Loop Internals\n\n> Source: https://hermes-agent.nousresearch.com/docs/developer-guide/agent-loop\n\n## Core Responsibilities\n\nThe `AIAgent` class in `run_agent.py` (~4,400 lines) orchestrates the agent's core operations:\n\n- Assembling system prompts and tool schemas via `prompt_builder.py`\n- Selecting the appropriate provider/API mode\n- Making interruptible model calls with cancellation support\n- Executing tool calls sequentially or concurrently\n- Maintaining conversation history in OpenAI message format\n- Handling compression, retries, and fallback model switching\n- Tracking iteration budgets across parent and child agents\n- Flushing persistent memory before context loss\n\n## Two Entry Points\n\n```python\n# Simple interface -- returns final response string\nresponse = agent.chat(\"Fix the bug in main.py\")\n\n# Full interface -- returns dict with messages, metadata, usage stats\nresult = agent.run_conversation(\n user_message=\"Fix the bug in main.py\",\n system_message=None, # auto-built if omitted\n conversation_history=None, # auto-loaded from session if omitted\n task_id=\"task_abc123\")\n```\n\n`chat()` wraps `run_conversation()` and extracts the `final_response` field.\n\n## API Modes\n\nThree execution modes resolve from provider selection, explicit arguments, and base URL heuristics:\n\n| API Mode | Used For | Client Type |\n|----------|----------|-------------|\n| `chat_completions` | OpenAI-compatible endpoints | `openai.OpenAI` |\n| `codex_responses` | OpenAI Codex/Responses API | `openai.OpenAI` |\n| `anthropic_messages` | Native Anthropic API | `anthropic.Anthropic` |\n\n**Resolution order:**\n1. Explicit `api_mode` constructor arg (highest priority)\n2. Provider-specific detection\n3. Base URL heuristics\n4. Default: `chat_completions`\n\n## Turn Lifecycle\n\nEach iteration follows this sequence:\n\n```\n1. Generate task_id if not provided\n2. Append user message to conversation history\n3. Build or reuse cached system prompt\n4. Check if preflight compression needed (>50% context)\n5. Build API messages from conversation history\n6. Inject ephemeral prompt layers (budget warnings, context pressure)\n7. Apply prompt caching markers if on Anthropic\n8. Make interruptible API call\n9. Parse response:\n - Tool calls: execute, append results, loop back to step 5\n - Text response: persist session, flush memory, return\n```\n\n### Message Format\n\nAll messages use OpenAI-compatible format internally:\n\n```json\n{\"role\": \"system\", \"content\": \"...\"}\n{\"role\": \"user\", \"content\": \"...\"}\n{\"role\": \"assistant\", \"content\": \"...\", \"tool_calls\": [...]}\n{\"role\": \"tool\", \"tool_call_id\": \"...\", \"content\": \"...\"}\n```\n\nReasoning content is stored in `assistant_msg[\"reasoning\"]` and displayed via callbacks.\n\n### Message Alternation Rules\n\n- After system message: `User -> Assistant -> User -> Assistant -> ...`\n- During tool calling: `Assistant (with tool_calls) -> Tool -> Tool -> ... -> Assistant`\n- Never two consecutive assistant messages\n- Never two consecutive user messages\n- Only `tool` role can have consecutive entries (parallel results)\n\n## Interruptible API Calls\n\nAPI requests run in background threads while monitoring interrupt events:\n\n```\nMain Thread API Thread\n |-- wait on: -> HTTP POST\n | - response to provider\n | - interrupt\n | - timeout\n```\n\nWhen interrupted:\n- The API thread is abandoned\n- Agent processes new input or shuts down cleanly\n- No partial response injected into history\n\n## Tool Execution\n\n### Sequential vs Concurrent\n\n- **Single tool call**: executed directly in main thread\n- **Multiple tool calls**: executed via `ThreadPoolExecutor`\n - Interactive tools (e.g., `clarify`) force sequential execution\n - Results reinserted in original tool call order\n\n### Execution Flow\n\n```\nFor each tool_call:\n 1. Resolve handler from tools/registry.py\n 2. Fire pre_tool_call plugin hook\n 3. Check if dangerous command\n - If dangerous: invoke approval_callback\n 4. Execute handler with args + task_id\n 5. Fire post_tool_call plugin hook\n 6. Append tool result to history\n```\n\n### Agent-Level Tools\n\nThese are intercepted before reaching `handle_function_call()`:\n\n| Tool | Why Intercepted |\n|------|-----------------|\n| `todo` | Reads/writes agent-local task state |\n| `memory` | Writes to persistent memory files |\n| `session_search` | Queries session history via agent DB |\n| `delegate_task` | Spawns subagents with isolated context |\n\n## Callback Surfaces\n\n| Callback | Fired When | Used By |\n|----------|-----------|---------|\n| `tool_progress_callback` | Before/after tool execution | CLI spinner, gateway |\n| `thinking_callback` | Model starts/stops thinking | CLI indicator |\n| `reasoning_callback` | Model returns reasoning | CLI display, gateway |\n| `clarify_callback` | `clarify` tool called | CLI prompt, gateway |\n| `step_callback` | After complete agent turn | Gateway, ACP |\n| `stream_delta_callback` | Each streaming token | CLI streaming display |\n| `tool_gen_callback` | Tool call parsed from stream | CLI tool preview |\n| `status_callback` | State changes | ACP updates |\n\n## Budget and Fallback Behavior\n\n### Iteration Budget\n\n- Default: 90 iterations (configurable via `agent.max_turns`)\n- Each agent gets independent budget; subagents capped at `delegation.max_iterations` (default 50)\n- At 100%, agent stops and returns work summary\n\n### Fallback Model\n\nWhen primary model fails (rate limit, server error, auth error):\n1. Check `fallback_providers` list\n2. Try each fallback in order\n3. Continue conversation with new provider on success\n4. Attempt credential refresh on 401/403 before failing over\n\nAuxiliary tasks have independent fallback chains via `auxiliary.*` config.\n\n## Compression and Persistence\n\n### When Compression Triggers\n\n- **Preflight**: Before API call if conversation exceeds 50% of context window\n- **Gateway auto-compression**: If exceeds 85% (more aggressive, runs between turns)\n\n### During Compression\n\n1. Memory flushed to disk first\n2. Middle conversation turns summarized\n3. Last N messages preserved intact (`compression.protect_last_n` default: 20)\n4. Tool call/result pairs kept together\n5. New session lineage ID generated\n\n### Session Persistence\n\nAfter each turn:\n- Messages saved to session store (SQLite via `hermes_state.py`)\n- Memory changes flushed to `MEMORY.md` / `USER.md`\n- Session resumable via `/resume` or `hermes chat --resume`\n\n## Key Source Files\n\n| File | Purpose |\n|------|---------|\n| `run_agent.py` | AIAgent class -- complete agent loop |\n| `agent/prompt_builder.py` | System prompt assembly |\n| `agent/context_engine.py` | ContextEngine ABC -- pluggable context management |\n| `agent/context_compressor.py` | Default engine -- lossy summarization |\n| `agent/prompt_caching.py` | Anthropic prompt caching markers |\n| `agent/auxiliary_client.py` | Auxiliary LLM client for side tasks |\n| `model_tools.py` | Tool schema collection, dispatch |\n",
"documents": []
},
"outgoingEdges": [],
"incomingEdges": [
{
"from": "page:docs-hermes-research",
"to": "page:docs-hermes-research-raw-agent-loop",
"kind": "contains_page"
}
]
}