Hmbown/CodeWhale · error · RuntimeContractError

{kind} metric `{'.'.join((*base, 'tools'))}` must equal the

Error message

{kind} metric `{'.'.join((*base, 'tools'))}` must equal the owned tool_names length ({len(names)})

What it means

For each mode x surface, the integer metric tool_catalog.modes.<mode>.<surface>.tools must exactly equal len(tool_names) in the same node. The count is deliberately redundant with the names list, and validate_identity_structure cross-checks the redundancy so a hand-edited budget cannot hide a tool-count change behind stale names. Raised from the same pass as the sorted-names check (validate_receipt, validate_budget, compare, budget_from_receipt, run_measurement).

Source

Thrown at scripts/check-runtime-contract-budget.py:227

            f"got {profile!r}"
        )

    for mode, _label in VISIBLE_MODES:
        for surface, _surface_label in TOOL_SURFACES:
            base = ("tool_catalog", "modes", mode, surface)
            names = required_value(document, (*base, "tool_names"), kind)
            dotted_names = ".".join((*base, "tool_names"))
            if (
                not isinstance(names, list)
                or any(not isinstance(name, str) or not name for name in names)
                or names != sorted(set(names))
            ):
                raise RuntimeContractError(
                    f"{kind} field `{dotted_names}` must be sorted unique non-empty strings"
                )
            count = metric_value(document, (*base, "tools"), kind)
            if count != len(names):
                raise RuntimeContractError(
                    f"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the "
                    f"owned tool_names length ({len(names)})"
                )
            digest = required_value(document, (*base, "identity_sha256"), kind)
            expected = tool_identity_digest(names)
            if digest != expected:
                raise RuntimeContractError(
                    f"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must "
                    "match the owned sorted tool_names"
                )

    for stage, _label in REPRESENTATIVE_STAGES:
        path = ("representative_context", "stages", stage, "identity_sha256")
        digest = required_value(document, path, kind)
        if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
            raise RuntimeContractError(
                f"{kind} field `{'.'.join(path)}` must be a lowercase SHA-256 digest"
            )

View on GitHub (pinned to 8880682c63)

Solutions

  1. Set the named tools metric to exactly len(tool_names) for that node
  2. If a tool was genuinely added, update tool_names, tools, bytes, tokens_est, and identity_sha256 as one unit in scripts/runtime-contract-budget.json
  3. Or re-measure with scripts/measure-runtime-contract.py and check the fresh receipt via --receipt so the harness computes all fields consistently

Example fix

// before
"tool_catalog": { "modes": { "act": { "full": {
  "tool_names": ["agent", "apply_patch", "bash"],
  "tools": 2 } } } }

// after
"tool_catalog": { "modes": { "act": { "full": {
  "tool_names": ["agent", "apply_patch", "bash"],
  "tools": 3 } } } }
Defensive patterns

Strategy: validation

Validate before calling

def tool_counts_consistent(doc: dict) -> bool:
    for mode in ("plan", "act", "operate"):
        for surface in ("full", "active"):
            node = (
                doc.get("tool_catalog", {})
                .get("modes", {})
                .get(mode, {})
                .get(surface, {})
            )
            names = node.get("tool_names")
            count = node.get("tools")
            if not isinstance(names, list) or count != len(names):
                return False
    return True

Prevention

When it happens

Trigger: A document where `tools` was edited (or freshly measured) without the matching tool_names edit - budget says "tools": 33 while tool_names lists 34 entries, or a name was appended without bumping the count. The message names the exact node and the expected length.

Common situations: Hand-applying a tool addition to the budget JSON and forgetting the count; copying a tools block between modes/surfaces whose name lists differ; a receipt from a measure-script version that counts differently (for example counting disabled tools).

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/e82ca4903cf483af. Report an issue: GitHub.