Skip to slide
Chapter 17 · Building the Full Mini-Harness
136 / 142

CHAPTER 17 · Building the Full Mini-Harness · 4 / 10

Step 2: wire the loop

agent.py brings every component together. Read it top to bottom; the comments name the chapter each piece comes from.

# minharness/agent.py
"""The integration layer: one agent loop wiring every chapter together."""
from .model import call_model
from .prompt import PromptBuilder, user_msg, tool_call, tool_result
from .context import Compactor, estimate_items_tokens
from .tools import ToolRegistry, Tool, shell_handler
from .approvals import ApprovalPolicy, gated_call
from .parallel import run_tools
from .memory import MemoryLoader
from .skills import SkillLoader
from .control import HookManager, Budget, BudgetExceeded
from .subagents import spawn_subagent, VERIFIER

class MiniHarness:
    def __init__(self, project_root: str, mode: str = "default"):
        # --- Chapter 6: the tool registry (the agent's hands) ---
        self.registry = ToolRegistry()
        self.registry.register(Tool(
            name="shell", description="Run a shell command",
            parameters={"type": "object",
                        "properties": {"command": {"type": "string"}},
                        "required": ["command"]},
            handler=shell_handler))

        # --- Chapter 7: the safety gate in front of every tool ---
        self.policy = ApprovalPolicy(allowlist=[r"^ls", r"^cat", r"^pytest"], mode=mode)

        # --- Chapter 9 + 10: project memory and skills feed the prompt ---
        self.memory = MemoryLoader(project_root)
        self.skills = SkillLoader(f"{project_root}/.skills")

        # --- Chapter 3 + 9 + 10: the prompt assembler ---
        self.prompt = PromptBuilder(
            base_instructions="You are minharness, a careful coding agent.",
            project_instructions=self.memory.load_instructions(project_root),
            skills_summary=self.skills.summary(),
            cwd=project_root,
            tools=self.registry.schemas())

        # --- Chapter 4 + 5: context management ---
        self.compactor = Compactor(max_tokens=8000, keep_recent=6)

        # --- Chapter 12: deterministic control + budget kill switch ---
        self.hooks = HookManager()
        self.hooks.register("PreToolUse", _block_dangerous)
        self.budget = Budget(max_turns=25, max_cost=5.0)

    def _execute_call(self, call: dict) -> str:
        """One tool call: hooks -> approval -> registry (Ch 12, 7, 6)."""
        veto = self.hooks.fire("PreToolUse", call)
        if veto.get("block"):
            return f"Exit code: 126\nBlocked by hook: {veto['reason']}"
        return gated_call(self.registry, self.policy, call["name"], call["args"],
                          unattended=(self.policy.mode == "full-auto"))

    def run(self, user_request: str) -> str:
        history = [user_msg(user_request)]                       # Chapter 3
        while True:
            history = self.compactor.compact(history)            # Chapter 5
            request = self.prompt.build_request(history)         # Chapter 3
            try:
                self.budget.charge(str(request), "")             # Chapter 12
            except BudgetExceeded as e:
                return f"Stopped: {e}"

            response = call_model(request)                       # Chapter 2

            if response["type"] == "text":                       # turn ends
                return response["content"]

            # one or more tool calls: run them (parallel-safe) -> append results
            calls = response["calls"]                            # Chapter 8
            for c in calls:
                history.append(tool_call(c["call_id"], c["name"], c["args"]))
            results = run_tools(calls, lambda c: self._execute_call(c))
            for r in results:
                history.append(tool_result(r["call_id"], r["output"]))

    def review(self, claim: str):                                # Chapters 11, 13
        """Optional: delegate an independent check to a verifier subagent."""
        return spawn_subagent(VERIFIER, f"Verify: {claim}",
                              lambda h, m: "VERIFIED (stub)", self.registry)

def _block_dangerous(payload):                                   # Chapter 12 hook
    cmd = payload.get("args", {}).get("command", "")
    if any(x in cmd for x in ("rm -rf", "git push", "curl")):
        return {"block": True, "reason": f"dangerous command: {cmd!r}"}
← → arrow keys work too