03 - The Agent Harness, Deep Dive
7/30/2026, 8:36:10 PM · updated 7/30/2026, 10:48:18 PM
A technical deep-dive into a minimal, hand-built agent harness—no frameworks—that walks through request authentication, system-prompt composition, wave-based tool orchestration, unified tool registration (hand-written + MCP), user memory management, ChromaDB retrieval with source-authority ranking, and the streaming-vs-non-streaming implementation trade-off.
This document details the architecture, request lifecycle, and core subsystems of apps/agent-server, a lean, hand-rolled agent harness built on FastAPI, httpx, ChromaDB, and the MCP SDK. No external frameworks are used; every layer—from authentication routing to memory management—is explicitly implemented to ensure predictable debugging and straightforward extension.
Request Lifecycle & Execution Flow
A request enters via POST /v1/chat/completions with a Bearer API key, formatted to match OpenAI's native API contract. Processing follows a strict sequence:
- Authentication runs before the request body is ever parsed.
auth.authenticate()validates that the bearer token's hash matches a known key inkeystore.py(which stores only SHA-256 hashes; raw keys are displayed once at creation and never recoverable), confirms the key is enabled and unexpired, verifies required scopes (chat,tools, oradmin), checks per-minute rate limits, and validates daily token quotas. - System prompt composition regenerates from scratch on every request.
compose_system_prompt()concatenates:- The base identity, tool-use policy, and guardrails from
AGENT.md(read directly from disk each time, allowing instant behavioral updates without restarts). - At most one matched skill block (keyword-driven via
skills/loader.py). - A plain-language memory briefing for the specific user (
_compose_memory_section(), pulling data frommemory_store.py).
- The base identity, tool-use policy, and guardrails from
- Self-referential routing intercepts the prompt before tool access is granted.
is_self_referential()determines whether the query targets the agent itself rather than domain content. If true, tool access is stripped for that request only (detailed below). - Orchestration executes in
graph.py, a lightweight custom engine that runs "waves" of work. All independent operations in a given step execute concurrently viaasyncio.gather. Results merge into a shared state object, and the next wave is assembled from each node's declared follow-ups. Tool calls issued by the model spawn individual nodes in the subsequent wave, ensuring truly parallel execution rather than sequential polling. - Tool hooks enforce lifecycle boundaries. A pre-tool hook can block execution entirely (e.g., enforcing write-only boundaries within the knowledge-bank folder). A post-tool hook triggers side effects (e.g., initiating background re-indexing after successful writes). Both are fault-tolerant: a hook error logs and skips rather than crashing the request.
- Model forwarding is abstracted into a single function.
_prepare_upstream_body()handles all formatting for the Ollama API call. This is the definitive location to modify model invocation logic (see07_Switching_LLM_Models_Guide.md). - Memory updates run asynchronously. Once the graph settles,
memory_pipeline.analyze_and_record()fires as a background task. It never blocks or delays the response the caller receives.
Streaming requests follow a parallel but duplicated path. Because streaming requires continuous token emission, it bypasses graph.py's wave-based final-state model. Instead, run_streaming() hand-rolls the same planner-then-tools loop directly, reusing the exact same hooks and tool registry to maintain behavioral parity. Note: This duplication means tool-round budgets and hook ordering are implemented twice and require manual synchronization—a documented maintenance overhead addressed later.
Tool Registry & External Integration
tools/registry.py maintains two parallel data structures: a list of tool schemas (exposed to the model) and a dictionary mapping names to executable Python functions. Adding native tools like search_knowledge_base or get_study_plan requires implementing the function and registering it in both structures.
External tools integrate via the Model Context Protocol (MCP). mcp_client.py:
- Parses
mcp_servers.json. - Launches configured servers as local subprocesses.
- Queries each for available tools and registers them into the unified registry under a
mcp__<server>__<tool>prefix.
This abstraction ensures agent_loop.py treats hand-written and external tools indistinguishably. Failure isolation is built in: if an MCP server fails to connect, it registers zero tools and emits a warning without disrupting core functionality. Currently, two configured servers (fetch, secedgar) remain disabled due to SDK incompatibility—a live demonstration of this failure boundary working as intended.
Active integrations:
filesystem(scoped to the knowledge-bank directory)sequentialthinking(structured planning/reasoning)pdfreadertavily(web search)
Current constraint: Only the stdio transport is supported. MCP servers requiring HTTP or SSE cannot yet be connected through this client.
Memory Management & State Rules
memory_store.py manages per-user facts (grade level, subject focus, documented knowledge gaps) in a partitioned SQLite database. Users are identified by caller-supplied identifiers, defaulting to the API key's ID if none is provided.
A critical architectural rule governs all writes:
Only explicit user input can update memory. Retrieved documents and model-generated text cannot write to or alter stored facts. This prevents hallucinated or retrieved claims from quietly becoming trusted state about the user.
Facts undergo a multi-day corroboration process before being promoted to stable status (trusted enough for confident assertion). Grade level specifically requires an explicit first-person statement from the user and can never be inferred from context.
Retrieval & Source Authority
Document retrieval in tools/knowledge_base.py follows a strict two-stage pipeline:
- Embedding & Search: Queries are embedded using
nomic-embed-text(the same model used for indexing) and matched against a ChromaDB collection via cosine similarity. This metric was deliberately chosen over the default distance metric because it measures vector direction rather than raw magnitude, which aligns better with dense text embeddings. - Source-Authority Re-ranking: A lightweight configurability layer nudge results toward higher-trust sources without overriding genuine relevance. This feature was added to resolve a real-world failure mode where secondary summary documents consistently outranked the primary regulations they were meant to support.
Self-Referential Routing & Guardrails
The routing gate acts as a low-cost filter before expensive reasoning begins:
- Regex Tier: A curated list checks for exact phrasings (e.g., "who are you," "what guardrails do you follow"). It strictly caps matching to short questions and excludes queries containing domain vocabulary (preventing false positives like budget adjustment questions that inadvertently use shared words).
- Embedding Tier: Triggered only if regex misses the query. The prompt is embedded, compared against canonical "about the agent" examples via cosine similarity, and evaluated against a 0.80 threshold. This value was empirically calibrated from logged similarity scores to maximize safe margin against false positives while catching genuine self-referential prompts.
Every routing decision (including near-misses) logs to logs/agents/triage.jsonl, enabling future threshold tuning against production data rather than relying on static configuration.
Code-enforced guardrails include:
- Write-path hooks that fail closed, blocking unrecognized or malformed write operations by default.
- Mandatory logging for every tool call.
- Strict credential isolation: the agent loop is never passed external credentials (e.g., Tailscale admin keys, backup drive access) and operates solely within its sandboxed folder.
Documented gap: No human-approval pause exists before a write executes. The folder sandbox remains the sole barrier against immediate execution of mistaken or manipulated outputs.
Architectural Trade-offs
The harness is built for predictability and extensibility, but carries intentional trade-offs:
| Strength | Weakness |
|---|---|
| Unified tool registry: Hand-written and MCP tools share identical interfaces, making new capabilities pluggable via a single registration line. | Duplicated execution paths: Streaming and non-streaming control flows are implemented separately, requiring manual synchronization of budgets, hooks, and rate limits. |
| Generic hook system: Pre/post events decouple side effects from tool logic, keeping the core loop unmodified during extensions. | In-process state management: Caches and rate limiters live exclusively in process memory, which will require refactoring once horizontal scaling or multiple instances are introduced. |
Both trade-offs are fully cataloged with resolution paths in 05_Gap_Analysis.md and 08_Extending_The_Harness_Agent.md.
Key takeaways
- Request processing is strictly sequential and transparent: Auth → prompt composition → routing check → wave-based orchestration → model forwarding → background memory update.
- Tools are framework-agnostic: A unified registry abstracts hand-written functions and MCP-integrated tools into identical callable schemas, with graceful failure isolation for external servers.
- Memory is strictly user-driven: ChromaDB retrieval and model generation never alter stored state; facts require multi-day corroboration to become stable.
- Guardrails are code-enforced, not aspirational: Writes fail closed, credentials never leave the sandbox, and tool calls are fully audited. Scaling and streaming path duplication remain the primary areas requiring architectural evolution.
Learning map
Learning Path: Building a Custom Agent Harness
Stage 1 — Foundations
- Understand agent architecture basics: router-prompt-model-loop pattern
- Set up a FastAPI server with Pydantic request/response models
- Configure an upstream LLM provider (Ollama, OpenAI, etc.)
Stage 2 — Request Pipeline
- Implement bearer-token auth with SHA-256 key hashing and scope/rate-limit checks
- Build dynamic system-prompt composer: base identity + matched skill + user profile briefing
- Add self-referential question filtering (regex + embedding tier)
Stage 3 — Tool System
- Design a dual source-tool registry: hand-written functions + MCP servers
- Register tool schemas (for the model) and handler functions (for execution)
- Implement pre/post hooks for validation and side effects
Stage 4 — Execution Engine
- Build the wave-based orchestrator using asyncio.gather for parallel tool calls
- Route result states back into the next wave's node graph
- Handle the streaming path separately with its own loop but shared tools/hooks
Stage 5 — Memory & Retrieval
- Set up per-user SQLite memory store (only user-typed facts, never model-generated)
- Embed and search ChromaDB with cosine similarity + source-authority re-ranking
- Promote short-lived facts to
Get hands-on — step by step
- Scaffold a FastAPI project and create a /v1/chat/completions endpoint that mirrors OpenAI's API shape.
- Build auth.authenticate() using keystore.py with SHA-256 hashing—store key hashes and allow keys to show only once at creation.
- Write compose_system_prompt() that reads AGENT.md from disk, loads one matched skill via skills/loader.py keyword overlap, and appends a memory briefing from memory_store.py.
- Create the unified tool registry in tools/registry.py: a schemas list (model-facing) and functions dict (execution-facing). Hand-register at least one tool like search_knowledge_base.
- Build mcp_client.py to read mcp_servers.json, launch each server as a subprocess, query available tools, and register them into the same registry under mcp__<server>__<tool> prefixes.
- Implement graph.py with wave-based orchestration: gather concurrent nodes per wave, merge results into shared state, schedule follow-up nodes until the model gives a final answer.
- Add pre-tool and post-tool hook chains in tools/registry.py for write-path validation (fail-closed) and side effects like background re-indexing.
- Create memory_store.py with SQLite per-user facts partitioned by caller ID or key—only update from user-typed text, promote to stable only after multi-day corroboration.
- Wire up ChromaDB retrieval in tools/knowledge_base.py using the same embedder as the index and cosine similarity for query matching, then add a source-authority re-ranking bonus.
- Implement run_streaming() that hand-rolls the planner-tools loop for token streaming—keep it consistent with the non-streaming graph.py by sharing the same registry, hooks, and tool budget logic.
- Add the self-referential routing gate: regex tier for exact phrasings + embedding tier (cosine against canonical "about me" questions) with a logged threshold that can be re-tuned over time.
Top 3 sources
- 1FastAPI Documentation
Official FastAPI docs—essential for building the OpenAI-compatible routing endpoint and request validation.
https://fastapi.tiangolo.com/
- 2Model Context Protocol (MCP) — Official Specification & SDK
The spec and Python SDK for MCP—the protocol used to discover, register, and call external tools from the agent harness.
https://modelcontextprotocol.io/
- 3ChromaDB Documentation
Official ChromaDB docs—covers embedding models, cosine similarity, collections, and retrieval queries used in this architecture.
https://docs.trychroma.com/
Links are AI-suggested — worth a quick sanity check before diving in.