hiveloomdocs

Extending

hiveloom's catalog is open: tools, guardrails, validators, loop policies, compaction methods, event hooks, and model providers are catalog entries, and extensions register new entries through one API. A registered entry shows up in hiveloom catalog, validates in specs like a builtin, and appears in the generator's meta-prompt — so hiveloom generate can weave harnesses with a capability the moment its pack is installed. MCP tools are the dynamic exception: they come from declared servers at run time and appear in hiveloom mcp list-tools, not hiveloom catalog.

Writing an extension

An extension is any module exposing hiveloom_extension(hive):

# my_ext.py
from hiveloom.ext import ExtensionAPI

def hiveloom_extension(hive: ExtensionAPI) -> None:
    # a plain function tool (schema derived from type hints)
    def ping() -> str:
        """Reply with pong."""
        return "pong"
    hive.register_function_tool(ping)

    # a configurable tool: factory(params, ctx) -> Tool; params come from the
    # spec entry `{builtin: slack_post, default_channel: "#ops"}`
    hive.register_tool(
        "slack_post", make_slack_tool,
        description="Post a message to a Slack channel.",
        tags=["network", "write"],
        params=[{"name": "default_channel", "type": "str"}],
    )

    hive.register_guardrail("pii_filter", make_pii_guardrail,
                            description="Block outputs containing PII.")
    hive.register_validator("json_diff", make_json_diff,
                            description="Diff output against a golden file.",
                            params=[{"name": "golden", "type": "str", "required": True}])
    hive.register_policy("reflexion", lambda p, c: ReflexionPolicy(),
                         description="One self-critique pass before finishing.")
    hive.register_compaction("keep_files", lambda p, c: KeepFilesCompaction(),
                             description="File-aware summarization.")
    hive.register_hook("audit_log", make_audit_handler,
                       description="Record every tool call to the audit sink.")
    hive.register_blueprint("scraper", "Always add no_network_write. Task: $ARGUMENTS")

    # ambient: runs for every harness in this process (e.g. org-wide audit)
    @hive.on("run_finished")
    def report(event):
        ...

Base classes to implement: hiveloom.tools.registry.Tool, hiveloom.guardrails.base.Guardrail, hiveloom.verify.base.Verifier, hiveloom.loop.policies.LoopPolicy, hiveloom.context.manager.CompactionMethod.

Where extensions load from

Source Scope Failure behavior
pip packages with a hiveloom.extensions entry point (packs) everywhere collected, never crashes (hiveloom extensions shows errors)
~/.hiveloom/extensions/*.py this user collected, never crashes
extensions: list in harness.yaml (paths or module names) that harness SpecError — the harness can't run without it

A pack declares its entry point in pyproject.toml:

[project.entry-points."hiveloom.extensions"]
acme-tools = "acme_tools:hiveloom_extension"

hiveloom package records the packs a spec's entries come from in hiveloom.lock (packs: with name + version), and a run against a missing pack fails with a message naming it. hiveloom extensions lists everything loaded plus any load errors.

The extensions: spec path is always frozen — evolution can never add or change the code a harness loads.

Model providers

model.provider is a registry name, not a hard-coded literal. Builtin providers already cover the major labs (claude, openai, gemini, mistral, deepseek, xai), the aggregators (groq, openrouter, together, fireworks), and local servers (ollama, vllm) — run hiveloom models to list them. models.md is the full reference: endpoints, key variables, pricing rules, open vs fixed catalogs, and ~/.hiveloom/models.yaml customisation.

What belongs here is the programmatic path, for anything that is not an OpenAI-compatible HTTP endpoint:

def setup(hive):
    hive.register_provider(
        "mylab",
        lambda ctx: MyProvider(),        # must return a ModelProvider
        api_key_env="MYLAB_API_KEY",
        base_url="https://api.mylab.example/v1",
        open_catalog=False,              # only the ids below validate
        models=[{"id": "mylab-small", "input_cost_per_mtok": 0.1,
                 "output_cost_per_mtok": 0.4}],
    )

Registering a name that already exists raises, so two packs cannot silently fight over one provider name; only models.yaml may override a builtin. Pricing lives in the registry, and unknown models fall back to conservative Haiku-class pricing so cost guardrails never under-count.

Generate/evolve can use any provider too: --model ollama/qwen3:32b (provider/model-id). Note model stays in ALWAYS_FROZEN — the registry widens what a human or generator may choose, never what evolution can mutate.

MCP servers

A harness can declare mcp_servers; their tools join the loop as ordinary tools (mcp__<server-name>__<tool>), discovered eagerly when the tool registry is built (including run --dry-run):

hiveloom add mcp-server --name search --stdio-command npx \
  --stdio-arg -y --stdio-arg @foo/mcp-search \
  --env-from-host API_KEY=FOO_SEARCH_API_KEY --dir ./h

hiveloom add mcp-server --name jira --url https://mcp.acme.com/mcp \
  --header-env 'Authorization=ACME_MCP_TOKEN' --tool search_issues --dir ./h

hiveloom mcp list-tools --dir ./h   # see what a declared server actually exposes

A stdio server (--stdio-command) is arbitrary local exec — the same trust boundary as any other code hook. mcp_servers is always frozen from evolution, the same risk class as extensions. A remote tool's own annotations (e.g. readOnlyHint/destructiveHint) are self-reported by an untrusted server and are never a security boundary — the real boundaries are the always-frozen set plus harness trust gating.

Lifecycle event hooks

One event taxonomy serves the spec's hooks: section, ambient hive.on(...) handlers, and the trace. Handlers get one dict payload; returning a dict steers the run, returning None observes:

Event A returned dict may…
context_assemble {"messages": [...]} — replace the message list
before_provider_request {"system": ...} / {"messages": [...]} / {"tools": [...]} — patch the outgoing request (this request only; runs after guardrails)
before_tool_call {"block": True, "reason": ...} or {"input": {...}}
after_tool_call {"content": ...} / {"is_error": ...} — patch the result
before_verification {"output": ...} — replace final output before guardrails and validators
before_compaction {"cancel": True} or {"summary": "..."}
run_started / before_model_call / after_provider_response / after_model_response / verification / run_finished observe only
hiveloom add hook --on before_tool_call --code hooks/audit.py:audit --dir ./h
hiveloom add hook --on before_verification --builtin strip_json_fence --dir ./h

(In harness.yaml the field is event: — unquoted on: is a YAML boolean.) Handlers must not raise; one that does is logged as a hook_error trace event and skipped. Guardrails remain the frozen safety layer and always run first; hooks are the extensible middleware layer.

Skills, deferred tools, tool ergonomics

Skills (progressive disclosure): hiveloom add skill pdf-report --description "..." scaffolds skills/pdf-report/SKILL.md and lists it in the spec. Only name + description enter the system prompt; the model reads the full file on demand with file_read.

Deferred tools: mark a tool deferred: true and it stays out of the model's payload until the auto-added search_tools tool activates it — keeps context small for harnesses with many tools.

Tool authors get: guidelines (usage rules injected while the tool is active), prepare(kwargs) (normalize model-mangled args before validation), run_with_updates(kwargs, on_update) (+supports_updates) for streaming progress as tool_update trace events, and ToolResult(terminate=True) to end the run without a final model call (honored when every result in the batch terminates).

Blueprints (generator house style)

A blueprint is a markdown fragment appended to hiveloom generate's meta-prompt — preferred tools, guardrail posture, validator patterns for a family of tasks. $ARGUMENTS/$@ expand to the task, $1..$9 to its words.

hiveloom generate "extract top HN stories" -o ./hn --blueprint scraper

Lookup order: ~/.hiveloom/blueprints/<name>.md, then pack-registered (hive.register_blueprint).

Trust

Harness folders carry executable code. Folders built on this machine (via init/construct/generate) are trusted automatically; a foreign folder (unzipped artifact, clone) is gated before any of its code loads:

hiveloom trust ./foreign-harness      # or: hiveloom run ./foreign --approve
HIVELOOM_TRUST=always hiveloom run .  # CI; `never` refuses instead

Decisions live in ~/.hiveloom/trust.json. Frozen paths protect a harness from its evolution; trust protects a machine from a foreign harness.

Embedding hiveloom

Any language — stream trace events as JSONL over stdout:

hiveloom run ./h --input notes.txt --stream
# {"type":"run_started",...}
# {"type":"tool_call",...}
# ...
# {"type":"run_result","ok":true,"status":"success",...}

Python — the semver-stable SDK:

from hiveloom import run_harness, generate_harness, Hive

result = run_harness("./h", "notes.txt", on_event=lambda e: print(e.type))

$HIVELOOM_HOME relocates everything user-level (extensions, models.yaml, blueprints, trust.json, the default Hive DB) — useful for tests and CI.