Chapter 1: The Harness Paradigm (Claude Code vs. Hermes Agent)
1. Pattern Summary
The harness paradigm is the foundational insight of production AI engineering: the model provides intelligence, but the harness provides control. A raw language model is a text generator — powerful but undirected, capable but unsafe. The harness is the infrastructure layer that wraps the model and transforms it into a controllable, auditable, production-ready agent. It constrains the action space through typed tools, manages conversation state across turns, enforces safety through a permission system, handles failures gracefully, and tracks resource consumption. Without a harness, deploying an LLM into production is like wiring a jet engine directly to a steering wheel — the power is there, but the control system is missing.
2. Claude Code Implementation
Claude Code implements the harness as a TypeScript class called QueryEngine. This class is the single owner of all mutable state in a conversation session, and every agent interaction flows through it.
The QueryEngine as harness core
// src/QueryEngine.ts
// The QueryEngine is the harness core — it owns all mutable state for a session.
export class QueryEngine {
private config: QueryEngineConfig
private mutableMessages: Message[] // Full conversation history, append-only
private abortController: AbortController // User can cancel mid-execution at any time
private permissionDenials: SDKPermissionDenial[] // Audit trail of every blocked action
private totalUsage: NonNullableUsage // Cumulative token + cost trackingEach field maps to a core harness responsibility. mutableMessages is the agent's memory — every user message, assistant response, and tool result is appended here. abortController is the emergency stop — the user can always regain control. permissionDenials is the audit trail — every blocked action is recorded for review. totalUsage is the budget tracker — the harness knows exactly what the session has cost.
submitMessage() as async generator
The QueryEngine exposes its functionality through a single async generator method:
// src/QueryEngine.ts
// submitMessage is an async generator — it yields results incrementally.
// This enables real-time streaming: the user sees text as it's generated,
// and tool executions are reported as they complete, not after the full turn.
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown>The async generator pattern is a deliberate design choice. Instead of blocking until the entire conversation turn is complete, submitMessage yields events as they happen — a token arrives, a tool starts, a tool finishes. This makes the system feel responsive and allows the UI layer to update in real time. It also means the harness can be interrupted mid-stream without losing the work already done.
The Tool interface as fundamental abstraction
Every action the model can take is expressed as a Tool. The interface is comprehensive by design — it forces every tool to declare not just what it does, but how it behaves:
// src/Tool.ts
// The Tool interface is the contract between the harness and every action.
// Tools must declare their identity, execution logic, input schema,
// concurrency safety, read/write/destructive behavior, and permission rules.
export type Tool<Input extends AnyObject = AnyObject, Output = unknown> = {
readonly name: string // How the model addresses this tool
call(args, context, canUseTool, ...): Promise<ToolResult<Output>> // Execution
readonly inputSchema: Input // Zod schema — validated before call()
isConcurrencySafe(input): boolean // Can this run in parallel?
isReadOnly(input): boolean // Safe to auto-approve?
isDestructive?(input): boolean // Requires explicit user consent?
checkPermissions(input, context): Promise<PermissionResult> // Tool-specific safety
maxResultSizeChars: number // Prevents context window explosions
}The inputSchema is a Zod schema — it provides both compile-time type safety and runtime validation. When the model produces a tool_use block, the harness validates the input against this schema before ever calling the tool. Malformed input never reaches tool code. The behavioral declarations (isReadOnly, isDestructive, isConcurrencySafe) let the harness make intelligent decisions about permissions and execution order without needing to understand what each tool actually does.
3. Hermes Agent Implementation
Hermes implements the harness as a Python class called AIAgent in run_agent.py. The design philosophy is similar — one class owns the session state — but the execution model is synchronous and the tool system uses a registry pattern rather than a typed interface.
AIAgent class structure
# run_agent.py
# AIAgent is the harness core in Hermes — it owns model, budget, callbacks, and platform context.
class AIAgent:
def __init__(
self,
model: str = "anthropic/claude-opus-4.6",
max_iterations: int = 90, # Hard cap on LLM turns
enabled_toolsets: List[str] = None,
disabled_toolsets: List[str] = None,
platform: str = None, # "cli", "telegram", "discord", etc.
iteration_budget: "IterationBudget" = None, # Shared across parent + subagents
tool_progress_callback: callable = None, # Real-time tool status updates
clarify_callback: callable = None, # Human-in-the-loop questions
session_id: str = None,
# ... plus provider, routing, memory, and checkpoint params
):
self.model = model
self.max_iterations = max_iterations
# IterationBudget is thread-safe and shared with subagents.
# Parent creates it; children inherit it. This caps total work
# across the entire agent tree, not just the root agent.
self.iteration_budget = iteration_budget or IterationBudget(max_iterations)
self.platform = platformIterationBudget: the Hermes safety valve
# run_agent.py
# IterationBudget is a thread-safe counter that caps runaway agent loops.
# It's shared across parent and all subagents — total iterations are bounded
# at the tree level, not per-agent. consume() returns False when the budget
# is exhausted, which breaks the agent loop cleanly.
class IterationBudget:
def __init__(self, max_total: int):
self.max_total = max_total
self._used = 0
self._lock = threading.Lock() # Thread-safe for parallel subagents
def consume(self) -> bool:
"""Try to consume one iteration. Returns True if allowed."""
with self._lock:
if self._used >= self.max_total:
return False
self._used += 1
return True
def refund(self) -> None:
"""Give back one iteration (e.g. for programmatic tool calls)."""
with self._lock:
if self._used > 0:
self._used -= 1run_conversation() as synchronous loop
# run_agent.py (simplified from AGENTS.md)
# run_conversation() is the harness loop — entirely synchronous.
# It calls the LLM, executes tools, and repeats until the model stops
# requesting tools or the iteration budget is exhausted.
def run_conversation(self, user_message: str, ...) -> dict:
while api_call_count < self.max_iterations and self.iteration_budget.remaining > 0:
response = client.chat.completions.create(
model=model, messages=messages, tools=tool_schemas
)
if response.tool_calls:
# Execute each tool call and append results to message history
for tool_call in response.tool_calls:
result = handle_function_call(tool_call.name, tool_call.args, task_id)
messages.append(tool_result_message(result))
api_call_count += 1
else:
# No more tool calls — model is done
return {"final_response": response.content, "messages": messages}Tool registry pattern
Where Claude Code uses a typed Tool interface, Hermes uses a central registry. Tools register themselves at import time:
# tools/registry.py (pattern from AGENTS.md)
# Tools self-register at import time. The registry collects schemas for the
# LLM's tool list and routes function_call events to the right handler.
# No interface to implement — just call registry.register() with a schema,
# a handler lambda, and an optional availability check function.
registry.register(
name="port_scan",
toolset="security",
schema={
"name": "port_scan",
"description": "Scan open ports on a target host",
"parameters": {"type": "object", "properties": {"host": {"type": "string"}}}
},
handler=lambda args, **kw: port_scan(host=args["host"], task_id=kw.get("task_id")),
check_fn=lambda: bool(os.getenv("NMAP_AVAILABLE")), # Availability guard
)The registry pattern trades compile-time safety for runtime flexibility. Tools can be added, removed, or conditionally enabled without changing any interface — just import the file and the tool appears. The check_fn provides the availability guard that Claude Code's checkPermissions handles at the interface level.



