The Host Is the Product: Runtime Engineering for LLM Agents
Every agent presentation shows the prompt. The prompt is 5% of the system. The other 95% is the host: the runtime that loads the model, manages the context window, executes tools, persists memory, enforces budgets, and decides when to stop. Xu et al. (2026) survey 200+ papers and conclude the unsolved problems are scheduling, recovery, state consistency, and cost control — all host problems. This post is a checklist for building a host that doesn’t embarrass you in production.
The Host Responsibilities (Exhaustive)
| Subsystem | What It Does | Failure If Missing |
|---|---|---|
| Model loader | Loads weights, applies chat template, manages KV cache | OOM, wrong format, silent degradation |
| Context manager | Assembles prompt: system + working memory + retrieved + history + headroom | Context rot, lost facts, budget overflow |
| Tool executor | Validates schemas, runs code, times out, retries, compresses results | Hanging calls, schema errors, token explosion |
| Memory system | Extraction (S), Storage (R), Retrieval (Q), Maintenance (U) Zhou et al. (2026) | Amnesia, stale facts, contradictions |
| Scheduler | Parallelizes independent calls, sequences dependent, enforces concurrency limits | Deadlocks, latency spikes, budget blowout |
| Budget enforcer | Token counter per turn, per session, per user; hard stops | Cost surprises, degraded quality |
| Checkpointing | Snapshots state (context + memory + tool results) every N turns | Unrecoverable failures, no replay |
| Eval harness | Runs snapshot tests Sun et al. (2025), regression suite, canary deployments | Silent regressions, hallucination drift |
| Observability | Logs: tokens/category, tool latency, retrieval precision, US/HR, cost | Blind debugging, no capacity planning |
| Deployment | Blue/green, rollback, feature flags, config versioning | Bad deploy = downtime |
Context Manager: The Heart
class ContextManager:
def __init__(self, budget: TokenBudget):
self.budget = budget
def assemble(self, turn: Turn) -> Prompt:
sections = [
("system", turn.system_prompt, Priority.CRITICAL),
("task_spec", turn.task_spec, Priority.CRITICAL),
("working_memory", turn.working_memory, Priority.HIGH),
("retrieved", self.retrieve(turn.query), Priority.HIGH),
("history", self.compress_history(turn.history), Priority.MEDIUM),
("few_shot", turn.few_shot, Priority.LOW),
]
return self.pack(sections, self.budget)
def pack(self, sections, budget):
# 1. Sort by priority
# 2. Fill from highest priority, truncate lowest
# 3. Preserve primacy/recency for retrieved + history
# 4. Return tokens_used for observability
Key invariants: System prompt never truncated. Task spec never truncated. Retrieved context sorted by rerank score. History compressed via summarization (not truncation).
Tool Executor: The Muscle
class ToolExecutor:
def __init__(self, registry: ToolRegistry, compressor: Compressor):
self.registry = registry
self.compressor = compressor
async def execute(self, calls: list[ToolCall]) -> list[ToolResult]:
# 1. Validate all schemas in parallel (fail fast)
# 2. Group: independent → parallel; dependent → sequence
# 3. Run with timeout + retry (exponential backoff)
# 4. Compress each result BEFORE returning to context
# 5. Log: call_id, tool, latency, tokens_in, tokens_out, status
Non-negotiable: Every tool has a compress(result, query, max_tokens) method. Raw tool output never enters context.
Memory System: The Four Modules
Following Zhou et al. (2026):
class MemorySystem:
def __init__(self):
self.R = HybridStorage(vector=pgvector, graph=kuzu, keyword=sqlite_fts)
self.S = SchemaExtractor(schema=FactSchema)
self.Q = HybridRouter(indices=["vector", "graph", "keyword"])
self.U = MaintenanceScheduler(jobs=[consolidate, dedupe, version])
def extract(self, dialogue: Dialogue) -> list[MemoryUnit]:
return self.S.extract(dialogue)
def store(self, units: list[MemoryUnit]):
self.R.write(units)
def retrieve(self, query: Query) -> list[MemoryUnit]:
return self.Q.route_and_search(query)
def maintain(self):
self.U.run_due_jobs()
Deployment reality: Start with vector + keyword. Add graph when temporal/entity queries exceed 20% of workload. Run U nightly.
Scheduler: Concurrency Without Chaos
class Scheduler:
def __init__(self, max_parallel: int = 3):
self.semaphore = asyncio.Semaphore(max_parallel)
self.dependency_graph = DependencyGraph()
async def run(self, plan: Plan) -> list[Result]:
# 1. Build DAG from plan (explicit deps or infer from tool signatures)
# 2. Execute ready nodes in parallel (semaphore-limited)
# 3. On failure: mark subgraph failed, continue independent branches
# 4. Support checkpoint: save DAG state + completed results
# 5. On retry: resume from checkpoint, re-run failed subgraph only
Key metric: Parallel efficiency = (sequential time) / (parallel time). Target > 2.5×.
Budget Enforcer: Tokens Are Money
class BudgetEnforcer:
def __init__(self, limits: Limits):
self.limits = limits # per_turn, per_session, per_user, per_day
self.usage = UsageTracker()
def check(self, scope: Scope, estimated: int) -> bool:
return self.usage.get(scope) + estimated <= self.limits.get(scope)
def record(self, scope: Scope, actual: int):
self.usage.add(scope, actual)
if self.usage.get(scope) > self.limits.get(scope) * 0.9:
alert("budget_near_limit", scope=scope, usage=self.usage.get(scope))
Hard stops: Per-turn (model max), per-session (user expectation), per-day (cost control). Soft alerts at 80%.
Checkpointing: Replayability
Every N turns (or on tool error), persist:
{
"turn": 12,
"context_hash": "sha256(...)",
"memory_snapshot_id": "mem_abc123",
"tool_results": [...],
"model_output": "...",
"budget_used": 45231
}
Enables: replay from any turn, A/B prompt testing on real trajectories, forensic debugging.
Eval Harness: Gate Every Deploy
# eval/gate.yml
stages:
- name: snapshot_regression
dataset: eval/snapshots/v3.jsonl
metric: utility_score
threshold: 0.70
- name: hallucination_rate
dataset: eval/snapshots/v3.jsonl
metric: hallucination_rate
threshold: 0.15
- name: memory_update
dataset: eval/memory/v1.jsonl
metric: update_accuracy
threshold: 0.85
- name: cost_per_task
dataset: eval/production_traces/last_week.jsonl
metric: p95_tokens
threshold: 8000
CI blocks on failure. Canary: 5% traffic → full metrics → promote/rollback.
Observability: The Dashboard You Need
| Panel | Query |
|---|---|
| Tokens per turn (p50/p95) | sum(tokens) by turn |
| Tool latency (p99) | histogram(tool_latency_ms) by tool |
| Retrieval precision@k | judge_score(retrieved, relevant) |
| Utility Score / HR | eval.mirage.us, eval.mirage.hr |
| Cost per session | sum(tokens * $/token) by session_id |
| Error rate by type | count(error_type) / total_turns |
| Checkpoint lag | now - last_checkpoint_ts |
The Host Checklist Before Launch
- Context budget defined + enforced + tested (ablation per category)
- Tool schemas versioned; compressor per tool; timeout/retries tuned
- Memory: extraction schema, hybrid storage, nightly maintenance job
- Scheduler: DAG support, checkpoint/resume, parallel efficiency > 2.5×
- Budget: per-turn/session/user/day limits + alerts at 80%
- Checkpoint: every 5 turns + on error; replay tested weekly
- Eval: snapshot gate (US ≥ 0.7, HR ≤ 0.15), memory gate, cost gate
- Observability: all 7 panels live; alerts on regression
- Deploy: blue/green, rollback < 2 min, config versioned
- Runbook: “agent goes rogue” → kill switch, memory quarantine, manual review
The Model Is Interchangeable
Swap GPT-4o → Claude 3.5 → Llama-3.1-405B. The host stays. The prompt templates adapt. The tool schemas stay. The memory system stays. The eval harness stays. The host is your product. The model is your supplier. Negotiate accordingly.