What invariants prevent silent skill degradation when tools are composed?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session composes multiple tools or capabilities into a workflow (tool A's output feeds tool B's input), and the composition may silently degrade the effective skill of the workflow below what any individual tool promised. Does not apply to single-tool workflows or compositions with verified equivalence to atomic capabilities.

Core Distinction

The separation between **individual tool quality** (each tool meets its individual accuracy, reliability, and semantics guarantees) and **composed workflow quality** (the chained output meets the workflow's requirements). Relying on individual quality creates the **Composition Skill Decay**: tool A is 90% accurate, tool B is 90% accurate, and the composed A→B workflow is 81% accurate (0.9 × 0.9), not 90%. The workflow silently degrades because errors from A propagate through B, and no one checked the composed accuracy.

What Was Observed

In tool-composition agent architectures, failing to verify composition skill produces three primary failure modes:

  1. **Accuracy Multiplication Collapse:** A 5-tool pipeline each with 95% accuracy produces 77% workflow accuracy. The user expects 95% because "each tool is accurate," but the workflow quietly operates at 77%.
  2. **Error Mode Introduction:** Tool A is deterministic and tool B is non-deterministic. Their composition introduces non-determinism into a workflow that was supposed to be deterministic, breaking downstream guarantees.
  3. **Semantic Contamination:** Tool A returns results in a slightly different format than tool B expects. B's error handling masks the mismatch, and the workflow produces plausible-looking wrong output.

Recorded Elements

  1. **`per_tool_quality_contract_receipt`:** For each composed tool, its cryptographic quality contract (accuracy, error rate, determinism, semantic guarantees).
  2. **`composition_quality_computation_signature`:** The computed quality of the composed workflow, including how per-tool qualities combine given the specific composition structure (chained, parallel, conditional).
  3. **`composition_failure_mode_manifest_receipt`:** The exact failure modes introduced or amplified by the composition, beyond what any single tool exhibits.
  4. **`composition_semantic_compatibility_signature`:** The cryptographic proof that tool A's output format matches tool B's expected input format (including error handling, edge cases).

Condition of Validity

* **Applies when:** Multiple tools are composed, their quality contracts are non-trivial, and workflow quality matters to downstream consumers. * **Does not apply when:** Single-tool workflows, or compositions where quality is verified as a unit.

Decision Boundary

  1. **Is the workflow a composition of multiple tools?** If no, single-tool quality applies. If yes, proceed.
  2. **Is `per_tool_quality_contract_receipt` present for each tool?** If yes, composition quality can be computed. If no, the composition is unquantified.
  3. **Does `composition_quality_computation_signature` show composed quality meeting the workflow requirement?** If yes, proceed. If no, the composition degrades below requirement.
  4. **Conversely, when can a composition proceed without quality computation?** Only when the workflow requirement is below the composition's minimum possible quality (conservative bound), or quality is not a requirement (best-effort workflow).

Operational Effect

  1. **Accuracy Collapse Detection:** Composition quality is computed, not assumed.
  2. **Error Mode Awareness:** Introduced error modes are surfaced.
  3. **Semantic Integrity:** Format compatibility is verified before composition.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class CompositionVerdict(Enum):
    SINGLE_TOOL = auto()
    COMPOSITION_MEETS_QUALITY = auto()
    COMPOSITION_DEGRADED = auto()
    SEMANTIC_INCOMPATIBLE = auto()

@dataclass(frozen=True)
class ToolQuality:
    tool_id: str
    accuracy: float
    determinism: str  # "DETERMINISTIC" | "NON_DETERMINISTIC"
    output_schema_hash: str
    expected_input_schema_hash: str

@dataclass(frozen=True)
class ToolComposition:
    composition_id: str
    tools: tuple[ToolQuality, ...]
    workflow_quality_requirement: float
    introduction_of_nondeterminism_acceptable: bool

@dataclass(frozen=True)
class CompositionPolicy:
    require_schema_compatibility: bool

@dataclass(frozen=True)
class CompositionManifest:
    verdict: CompositionVerdict
    composed_quality: float
    directive: str
    manifest_digest: str

class ToolCompositionSkillGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_composition(
        cls, comp: ToolComposition, policy: CompositionPolicy
    ) -> CompositionManifest:
        
        # Gate 1: Single tool
        if len(comp.tools) <= 1:
            q = comp.tools[0].accuracy if comp.tools else 1.0
            digest = cls._hash(json.dumps({
                "composition": comp.composition_id,
                "tools": str(len(comp.tools)),
                "quality": f"{q:.3f}"
            }, sort_keys=True))
            return CompositionManifest(
                verdict=CompositionVerdict.SINGLE_TOOL,
                composed_quality=q,
                directive=f"PROCEED: Single tool workflow, quality {q:.2f}.",
                manifest_digest=digest
            )

        # Gate 2: Schema compatibility
        if policy.require_schema_compatibility:
            for i in range(len(comp.tools) - 1):
                upstream = comp.tools[i]
                downstream = comp.tools[i + 1]
                if upstream.output_schema_hash != downstream.expected_input_schema_hash:
                    return CompositionManifest(
                        verdict=CompositionVerdict.SEMANTIC_INCOMPATIBLE,
                        composed_quality=0.0,
                        directive=f"HALT: {upstream.tool_id} output schema != {downstream.tool_id} input schema.",
                        manifest_digest=cls._hash(f"SCHEMA:{i}")
                    )

        # Gate 3: Non-determinism introduction
        has_deterministic = any(t.determinism == "DETERMINISTIC" for t in comp.tools)
        has_nondeterministic = any(t.determinism == "NON_DETERMINISTIC" for t in comp.tools)

        if has_deterministic and has_nondeterministic and not comp.introduction_of_nondeterminism_acceptable:
            return CompositionManifest(
                verdict=CompositionVerdict.COMPOSITION_DEGRADED,
                composed_quality=0.0,
                directive=f"HALT: Mixing deterministic and non-deterministic tools without explicit acceptance.",
                manifest_digest=cls._hash("NONDET_INTRODUCED")
            )

        # Gate 4: Quality composition (chained: multiply accuracies)
        composed_quality = 1.0
        for tool in comp.tools:
            composed_quality *= tool.accuracy

        if composed_quality < comp.workflow_quality_requirement:
            return CompositionManifest(
                verdict=CompositionVerdict.COMPOSITION_DEGRADED,
                composed_quality=composed_quality,
                directive=f"HALT: Composed quality {composed_quality:.2%} < requirement {comp.workflow_quality_requirement:.2%}.",
                manifest_digest=cls._hash(f"DEGRADED:{composed_quality:.3f}")
            )

        # Gate 5: Meets quality
        digest = cls._hash(json.dumps({
            "composition": comp.composition_id,
            "tools": tuple(t.tool_id for t in comp.tools),
            "composed_quality": f"{composed_quality:.3f}",
            "requirement": f"{comp.workflow_quality_requirement:.3f}"
        }, sort_keys=True))
        
        return CompositionManifest(
            verdict=CompositionVerdict.COMPOSITION_MEETS_QUALITY,
            composed_quality=composed_quality,
            directive=f"PROCEED: {len(comp.tools)}-tool composition, quality {composed_quality:.2%}.",
            manifest_digest=digest
        )