BrainBank

08 - Extending the Harness Agent

7/30/2026, 9:29:45 PM · updated 7/30/2026, 10:47:42 PM

#agent-architecture#step-by-step#tool-registration#mcp-servers#skills-system#hooks-pattern#extension-design

A practical guide to extending the Harness Agent system through its five built-in extension points — tools, MCP servers, skills, hooks, and models — plus structural improvements worth building next.

This document serves as a practical implementation guide for extending the apps/agent-server architecture. Designed around an additive extension model, the system allows new capabilities to be integrated via small configuration files and registration snippets without modifying the core request or execution loops. Below is a reference for existing extension paths, followed by architectural improvements that would strengthen the system long-term.

Extending with Local Tools

Adding a local tool is the most straightforward extension path. Create tools/your_tool.py exporting two components:

  • A SCHEMA dictionary defining the OpenAI function-calling schema (argument descriptions, data types, and constraints).
  • An async def implementation function that executes the tool's logic.

Register the tool in tools/registry.py by importing the module and adding one entry each to the SCHEMAS list and _FUNCTIONS name-to-callable dictionary. Because agent_loop.py and graph.py interact with tools exclusively through the registry, they require zero source modifications.

Injecting Trusted Context: If a tool requires server-supplied context (e.g., authenticated user identity or session state), declare a parameter matching the expected name (following the existing partition_key pattern used by memory-aware tools). The call_tool() method inspects the function's real signature and injects these values automatically. Model-provided arguments with identical names cannot override this; server-supplied context always takes precedence, preserving a critical security boundary against LLM spoofing.

Extending with MCP Servers

When a capability already exists as a Model Context Protocol (MCP) server—ranging from web search to domain-specific data sources—wiring it up typically requires less effort than writing a custom tool manually. Add an entry to apps/agent-server/mcp_servers.json (not the .example.json companion, which is strictly inert reference documentation). Each entry follows this structure:

{
  "name": "...",
  "command": "...",
  "args": [...],
  "env": {...}
}

Restart the server and verify connectivity via the /health endpoint (mcp_tools_registered count).

Pre-flight Checklist: Always smoke-test the server locally (npx <package> or uvx <package>, depending on distribution) before wiring it into the project. Two pre-configured servers (fetch, secedgar) failed on import against the installed SDK despite installing cleanly; testing prevents assumption-based deployment failures. Consult MCP_SERVERS_CATALOG.md for a researched shortlist across web search, PDF handling, structured planning, and financial data before researching new options from scratch.

Known Constraints:

  • Only stdio transport is supported. HTTP or SSE-only MCP servers cannot be wired without building a custom transport layer first (a scoped project if a specific tool demands it).
  • Isolation is built-in: a failing server registers zero tools, logs a warning, and allows other servers to start normally. Speculative additions are genuinely low-risk.

Extending with Skills

Skills represent the lowest-friction extension point in the architecture. Simply drop a Markdown file into apps/agent-server/skills/ containing YAML frontmatter (description: ...) followed by the guidance text you want injected into the system prompt upon match. No registration code is required—the file's existence acts as the wiring.

Skills are matched via keyword overlap against the user's message, with a hard limit of exactly one skill injected per request. If your new skill shares vocabulary with an existing one, fold the guidance into the original file to prevent silent match failures rather than creating overlapping files that will never win the injection slot.

Note: A server restart is required to register new or modified skill files. Unlike AGENT.md (which is re-read fresh on every request), skills are cached at process startup.

Extending with Hooks

Hooks execute at precisely two lifecycle moments: immediately before a tool executes, and immediately after a real (non-blocked) tool call returns. Create hooks/your_hook.py, define the appropriate signature, and register it via register_pre_tool() or register_post_tool(). Add a single import line to hooks/__init__.py to activate it.

Hook Signatures:

  • Pre-tool hook: fn(tool_name, arguments) -> dict | None. Returning a dict blocks the actual tool call and returns that dictionary as the result (ideal for policy enforcement, mirroring the existing write-path guard).
  • Post-tool hook: fn(tool_name, arguments, result) -> None. Designed purely for side effects (e.g., triggering a background re-index after a successful write, using the knowledge-base auto-sync hook as a template).

All hooks run defensively: a single hook raising an exception is caught, logged, and skipped. Bugs isolate to the hook itself rather than crashing the active request or service.

Using or Adding New Models

Model routing and switching are fully managed via configuration passthrough to Ollama. No source code changes are required. For detailed configuration and selection strategies, consult 07_Switching_LLM_Models_Guide.md.

Architectural Improvements Worth Prioritizing

Beyond the additive extension points above, several structural enhancements would meaningfully strengthen the system. These are listed in rough order of leverage:

EnhancementDescription & Implementation Path
Server-side model routingCurrently requires explicit caller assignment. Implement a lightweight router early in agent_loop.run() / run_streaming() (before _prepare_upstream_body()) to route by capability (e.g., auto-select vision when images are present) or latency profiles. A clean seam already exists for this logic.
Unified streaming/non-streaming pathsBoth execution modes currently duplicate the planner-then-tools control flow, forcing double-maintenance for future changes to tool budgeting, hooks, or memory recording. Refactor graph.py's wave executor to support incremental/native stream output. High effort, high long-term payoff.
Skills matcher routing improvementApply the Step 5.9/10 regex-then-embedding-similarity gate directly to skills/loader.py. The embedding infrastructure already exists and is used elsewhere; applying this pattern eliminates identical false-positive matching issues. Excellent structural exercise for understanding the codebase routing layers.
Human-approval write gateThe current guard restricts where writes occur but lacks a pause-for-confirmation mechanism. A minimal implementation (log intended action → require second explicit tool call to confirm execution) would close workflow gaps without requiring external UI or notification systems.
Shared cache / rate-limit layerSpeculative now, but necessary for horizontal scaling. The in-memory rate limiter, MCP session pool, and multiple caches must migrate to a shared backend (Redis is the standard choice; properly locked SQLite functions adequately at this scale). Map out the architecture before scaling horizontally beyond a single process.

Guiding Principle for System Extensions

Every extension point exists because the original architecture deliberately separated "the thing that changes" (tools, skills, hooks, MCP servers, models) from "the thing that doesn't" (request pipeline, graph executor, auth layer).

When building an extension, the strongest validation of your approach is that it requires writing exactly one new file and a few lines of registration. If an extension demands modification to agent_loop.py's core control flow or graph.py's executor itself, pause to evaluate whether the registry/hook/skill pattern can be expanded first. This deliberate separation is what has kept the codebase compact, auditable, and stable across eight build phases in four days.

Key Takeaways

  • Additive-by-design: New capabilities integrate via small, isolated files and registry bindings without touching core orchestration loops.
  • Server-enforced security: Trusted context injection uses real Python function signatures, making LLM argument spoofing structurally impossible.
  • Hard limits exist: MCP relies exclusively on stdio transport; skills cache at startup and inject exactly one match per request based on keyword overlap.
  • Hooks fail gracefully: Both pre- and post-tool hooks run defensively, isolating exceptions from the main execution pipeline.
  • Prioritize structural leverage: Route models server-side, unify streaming paths before budgeting changes, and plan shared caching architecture early to avoid scaling debt.

Learning map

Extending the Harness Agent — Learning Roadmap

Phase 1 — Fundamentals (Understanding the Architecture)

  • Understand the core design principle: keep "the thing that changes" separate from "the thing that doesn't"
  • Explore agent_loop.py and graph.py to see what the core loop handles
  • Study tools/registry.py — the central pattern all extensions follow

Phase 2 — Additive Extensions (Zero Core Changes)

  1. Add a local tool — write tools/your_tool.py, register in registry, test via /health
  2. Add an MCP server — edit mcp_servers.json, smoke-test in Terminal, confirm tool registration
  3. Add a skill — drop a Markdown file with YAML frontmatter into skills/ (no code needed)
  4. Add a hook — write pre-tool or post-tool logic, register via hooks/__init__.py

Phase 3 — Model Integration

  • Configure model passthrough to Ollama (no code change required)
  • See 07_Switching_LLM_Models_Guide.md for full details

Phase 4 — Structural Enhancements (Bigger Projects)

  • Implement server-side model routing (agent_loop.run())
  • Unify streaming and non-streaming request paths in graph.py
  • Reuse the regex-then-embedding routing pattern for skills matching
  • Add a human-approval gate for file writes
  • Plan for shared cache/rate-limit layer before horizontal scaling

Get hands-on — step by step

  1. Explore the directory structure: run ls apps/agent-server/tools/, ls apps/agent-server/skills/, and ls apps/agent-server/hooks/ to see existing extension files.
  2. Create a new local tool: write tools/calendar_query.py with a SCHEMA dict describing its arguments and an async function implementing the logic.
  3. Register your tool: open tools/registry.py, import your module, and add one entry each to the SCHEMAS list and _FUNCTIONS dictionary.
  4. Restart agent-server and verify via GET /health that your tool appears in the registered tool count.
  5. Add a new MCP server: edit mcp_servers.json with an entry like {"name": "fetch", "command": "npx", "args": ["-y", "/path/to/package"], "env": {}} — do NOT edit the .example.json file.
  6. Smoke-test the MCP package directly in Terminal before wiring it: run npx fetch --version or uvx fetch --version to confirm it runs.
  7. Restart agent-server and check /health's mcp_tools_registered count to confirm connection.
  8. Create a new skill: write skills/your_skills_name.md with YAML frontmatter (description: "...") followed by guidance text — no registration code needed.
  9. Test your skill by sending a user message containing keywords from the description and verify it gets injected into the system prompt.
  10. Add a hook: write hooks/log_tool_usage.py, define fn(tool_name, arguments, result) -> None for post-tool logging, call register_post_tool(), then add one import line to hooks/__init__.py.
  11. Verify the hook works safely — remember hooks that raise exceptions get logged and skipped without breaking the request.

Top 3 sources

  1. 1
    OpenAI function calling documentation

    The canonical reference for OpenAI function-calling schemas — defines what a SCHEMA dict should contain and how the model decides when to call tools.

    https://platform.openai.com/docs/guides/function-calling

  2. 2
    Model Context Protocol (MCP) Specification

    The official MCP specification — explains server transport types (stdio vs HTTP/SSE), tool protocols, and the standard server format used in mcp_servers.json.

    https://github.com/modelcontextprotocol/specification

  3. 3
    LangChain Agent Patterns

    A well-known agent framework demonstrating tool integration patterns, hook-like callbacks, and extensibility architectures similar to those in Harness Agent.

    https://python.langchain.com/docs/modules/agents/

Links are AI-suggested — worth a quick sanity check before diving in.